Compare commits

..

30 Commits

Author SHA1 Message Date
Ralph Khreish
565090257c chore: fix format 2025-08-02 23:51:53 +03:00
Ralph Khreish
2c27aeda20 chore: fix tag-extension package.json not found for extension 2025-08-02 23:50:58 +03:00
github-actions[bot]
9d5f50ac8e Version Packages (#1072)
* Version Packages

* chore: add eyal instead of crunchyman to eyal's feature

* chore: run format

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-08-02 22:43:08 +02:00
Ralph Khreish
bbeaa9163a chore: exit pre (#1071) 2025-08-02 22:35:46 +02:00
Ralph Khreish
a4a172be94 Release 0.23.0 #1064 from eyaltoledano/next
Release 0.23.0
2025-08-02 23:18:05 +03:00
github-actions[bot]
028ed9c444 chore: rc version bump 2025-08-02 20:07:43 +00:00
Ralph Khreish
53903f1e8e chore: trick rc bump 2025-08-02 23:06:37 +03:00
github-actions[bot]
36c56231cc chore: rc version bump 2025-08-02 20:04:33 +00:00
Ralph Khreish
b82d858f81 chore: rename chagneset for rc bump 2025-08-02 23:03:10 +03:00
Ralph Khreish
9808967d6b chore: change from patch to minor 2025-08-02 23:00:56 +03:00
Ralph Khreish
3fee7515f3 fix: fix mcp tool call in extension (#1070)
* fix: fix mcp tool call in extension

- fix console.log directly being used in scope-adjutment.js breaking mcp

* chore: run format and fix tests

* chore: format
2025-08-02 21:59:02 +02:00
github-actions[bot]
82b17bdb57 chore: rc version bump 2025-08-02 16:44:19 +00:00
Eyal Toledano
72ca68edeb Task 104: Implement 'scope-up' and 'scope-down' CLI Commands for Dynamic Task Complexity Adjustment (#1069)
* feat(task-104): Complete task 104 - Implement scope-up and scope-down CLI Commands

- Added new CLI commands 'scope-up' and 'scope-down' with comma-separated ID support
- Implemented strength levels (light/regular/heavy) and custom prompt functionality
- Created core complexity adjustment logic with AI integration
- Added MCP tool equivalents for integrated environments
- Comprehensive error handling and task validation
- Full test coverage with TDD approach
- Updated task manager core and UI components

Task 104: Implement 'scope-up' and 'scope-down' CLI Commands for Dynamic Task Complexity Adjustment - Complete implementation with CLI, MCP integration, and testing

* chore: Add changeset for scope-up and scope-down features

- Comprehensive user-facing description with usage examples
- Key features and benefits explanation
- CLI and MCP integration details
- Real-world use cases for agile workflows

* feat(extension): Add scope-up and scope-down to VS Code extension task details

- Added useScopeUpTask and useScopeDownTask hooks in useTaskQueries.ts
- Enhanced AIActionsSection with Task Complexity Adjustment section
- Added strength selection (light/regular/heavy) and custom prompt support
- Integrated scope buttons with proper loading states and error handling
- Uses existing mcpRequest handler for scope_up_task and scope_down_task tools
- Maintains consistent UI patterns with existing AI actions

Extension now supports dynamic task complexity adjustment directly from task details view.
2025-08-02 18:43:04 +02:00
DavidMaliglowka
64302dc191 feat(extension): complete VS Code extension with kanban board interface (#997)
---------
Co-authored-by: DavidMaliglowka <13022280+DavidMaliglowka@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-01 14:04:22 +02:00
github-actions[bot]
60c03c548d chore: rc version bump 2025-07-31 22:15:10 +00:00
Ralph Khreish
2ae6e7e6be fix: normalize task IDs to numbers on load to fix comparison issues (#1063)
* fix: normalize task IDs to numbers on load to fix comparison issues

When tasks.json contains string IDs (e.g., "5" instead of 5), task lookups
fail because the code uses parseInt() and strict equality (===) for comparisons.

This fix normalizes all task and subtask IDs to numbers when loading the JSON,
ensuring consistent comparisons throughout the codebase without requiring
changes to multiple comparison locations.

Fixes task not found errors when using string IDs in tasks.json.

* Added test

* Don't mess up formatting

* Fix formatting once and for all

* Update scripts/modules/utils.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update scripts/modules/utils.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update scripts/modules/utils.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: normalize task IDs to numbers on load to fix comparison issues

- Added normalizeTaskIds function to convert string IDs to numbers
- Applied normalization in readJSON for all code paths
- Fixed set-task-status, add-task, and move-task to normalize IDs when working with raw data
- Exported normalizeTaskIds function for use in other modules
- Added test case for string ID normalization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Simplified implementation

* refactor: normalize IDs once when loading JSON instead of scattered calls

- Normalize all tags' data when creating _rawTaggedData in readJSON
- Add support for handling malformed dotted subtask IDs (e.g., "5.1" -> 1)
- Remove redundant normalizeTaskIds calls from set-task-status, add-task, and move-task
- Add comprehensive test for mixed ID formats (string IDs and dotted notation)
- Cleaner, more maintainable solution that normalizes IDs at load time

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: run format to resolve CI issues

---------

Co-authored-by: Carl Mercier <carl@carlmercier.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-01 00:06:51 +02:00
Ben Vargas
45a14c323d fix: remove default value from complexity report option to enable tag-specific detection (#1049)
Removes the default empty array value from the complexity report option to properly detect when tags are explicitly provided vs when no tags are provided, fixing the expand --all command behavior with tagged tasks.

Co-authored-by: Ben Vargas <ben@example.com>
2025-07-26 15:26:45 +02:00
github-actions[bot]
29e67fafa4 Version Packages (#1050)
* Version Packages

* chore: fix release 0.22

todo: fix CI

* chore: run format

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-07-26 00:45:07 +02:00
Ralph Khreish
43e4d7c9d3 Release 0.22 #1038 from eyaltoledano/next
Release 0.22
2025-07-26 01:24:38 +03:00
Ralph Khreish
1bd1e64cac feat: add pull request templates for bug fixes, features, and integrations (#1044)
* feat: add pull request templates for bug fixes, features, and integrations

- Introduced a comprehensive pull request template structure to streamline contributions.
- Added specific templates for bug fixes, new features, and integrations to enhance clarity and consistency in PR submissions.
- Configured the pull request template settings for better user guidance during the contribution process.

* chore: fix format

* Update .github/PULL_REQUEST_TEMPLATE.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update .github/PULL_REQUEST_TEMPLATE.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update .github/PULL_REQUEST_TEMPLATE.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: implement PR requested changes

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-07-24 17:33:43 +02:00
Ralph Khreish
dc44ed9de8 feat: Improved the CLI user interface to suggest generating a complexity report if it is missing, instead of showing an error. (#1043)
* feat: fix CLI UI error when trying to display non-existent complexity report

* chore: fix git issue

* chore: run format

* Update .changeset/thick-squids-attend.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update .changeset/thick-squids-attend.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update .changeset/thick-squids-attend.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-07-24 15:50:18 +02:00
github-actions[bot]
31b8407dbc chore: rc version bump 2025-07-23 16:29:49 +00:00
Ralph Khreish
2df4f13f65 chore: improve pre-release CI to be able to release more than one release candidate (#1036)
* chore: improve pre-release CI to be able to release more than one release candidate

* chore: implement requested changes from coderabbit

* chore: apply requested changes
2025-07-23 18:28:17 +02:00
github-actions[bot]
a37017e5a5 docs: Auto-update and format models.md 2025-07-23 16:03:40 +00:00
Ralph Khreish
fb7d588137 feat: improve config-manager max tokens for openrouter and kimi-k2 model (#1035) 2025-07-23 18:03:26 +02:00
Ralph Khreish
bdb11fb2db chore: remove useless file 2025-07-23 18:04:13 +03:00
Ralph Khreish
4423119a5e feat: Add Kiro hooks and configuration for Taskmaster integration (#1032)
* feat: Add Kiro hooks and configuration for Taskmaster integration

- Introduced multiple Kiro hooks to automate task management workflows, including:
  - Code Change Task Tracker
  - Complexity Analyzer
  - Daily Standup Assistant
  - Git Commit Task Linker
  - Import Cleanup on Delete
  - New File Boilerplate
  - PR Readiness Checker
  - Task Dependency Auto-Progression
  - Test Success Task Completer
- Added .mcp.json configuration for Taskmaster AI integration.
- Updated development workflow documentation to reflect new hook-driven processes and best practices.

This commit enhances the automation capabilities of Taskmaster, streamlining task management and improving developer efficiency.

* chore: run format

* chore: improve unit tests on kiro rules

* chore: run format

* chore: run format

* feat: improve PR and add changeset
2025-07-23 17:02:16 +02:00
Ben Vargas
7b90568326 fix: bump ai-sdk-provider-gemini-cli to v0.1.1 (#1033)
* fix: bump ai-sdk-provider-gemini-cli to v0.1.1

Updates ai-sdk-provider-gemini-cli from v0.0.4 to v0.1.1 to fix a breaking change
introduced in @google/gemini-cli-core v0.1.12+ where createContentGeneratorConfig
signature changed, causing "config.getModel is not a function" errors.

The new version includes:
- Fixed compatibility with @google/gemini-cli-core ^0.1.13
- Added proxy support via configuration
- Resolved the breaking API change

Fixes compatibility issues when using newer versions of gemini-cli-core.

See: https://github.com/ben-vargas/ai-sdk-provider-gemini-cli/releases/tag/v0.1.1

* chore: fix package-lock.json being too big

---------

Co-authored-by: Ben Vargas <ben@example.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-07-23 17:01:59 +02:00
github-actions[bot]
9b0630fdf1 docs: Auto-update and format models.md 2025-07-22 18:15:35 +00:00
Parthy
ced04bddd3 docs(models): update model configuration to add supported field (#1030) 2025-07-22 20:15:22 +02:00
134 changed files with 24979 additions and 897 deletions

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix 'expand --all' and 'show' commands to correctly handle tag contexts for complexity reports and task display.

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Clean up remaining automatic task file generation calls

View File

@@ -1,24 +0,0 @@
---
"task-master-ai": minor
---
Add comprehensive Kiro IDE integration with autonomous task management hooks
- **Kiro Profile**: Added full support for Kiro IDE with automatic installation of 7 Taskmaster agent hooks
- **Hook-Driven Workflow**: Introduced natural language automation hooks that eliminate manual task status updates
- **Automatic Hook Installation**: Hooks are now automatically copied to `.kiro/hooks/` when running `task-master rules add kiro`
- **Language-Agnostic Support**: All hooks support multiple programming languages (JS, Python, Go, Rust, Java, etc.)
- **Frontmatter Transformation**: Kiro rules use simplified `inclusion: always` format instead of Cursor's complex frontmatter
- **Special Rule**: Added `taskmaster_hooks_workflow.md` that guides AI assistants to prefer hook-driven completion
Key hooks included:
- Task Dependency Auto-Progression: Automatically starts tasks when dependencies complete
- Code Change Task Tracker: Updates task progress as you save files
- Test Success Task Completer: Marks tasks done when tests pass
- Daily Standup Assistant: Provides personalized task status summaries
- PR Readiness Checker: Validates task completion before creating pull requests
- Complexity Analyzer: Auto-expands complex tasks into manageable subtasks
- Git Commit Task Linker: Links commits to tasks for better traceability
This creates a truly autonomous development workflow where task management happens naturally as you code!

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix VSCode profile generation to use correct rule file names (using `.instructions.md` extension instead of `.md`) and front-matter properties (removing the unsupported `alwaysApply` property from instructions files' front-matter).

View File

@@ -0,0 +1,803 @@
---
description:
globs:
alwaysApply: true
---
# Test Workflow & Development Process
## **Initial Testing Framework Setup**
Before implementing the TDD workflow, ensure your project has a proper testing framework configured. This section covers setup for different technology stacks.
### **Detecting Project Type & Framework Needs**
**AI Agent Assessment Checklist:**
1. **Language Detection**: Check for `package.json` (Node.js/JavaScript), `requirements.txt` (Python), `Cargo.toml` (Rust), etc.
2. **Existing Tests**: Look for test files (`.test.`, `.spec.`, `_test.`) or test directories
3. **Framework Detection**: Check for existing test runners in dependencies
4. **Project Structure**: Analyze directory structure for testing patterns
### **JavaScript/Node.js Projects (Jest Setup)**
#### **Prerequisites Check**
```bash
# Verify Node.js project
ls package.json # Should exist
# Check for existing testing setup
ls jest.config.js jest.config.ts # Check for Jest config
grep -E "(jest|vitest|mocha)" package.json # Check for test runners
```
#### **Jest Installation & Configuration**
**Step 1: Install Dependencies**
```bash
# Core Jest dependencies
npm install --save-dev jest
# TypeScript support (if using TypeScript)
npm install --save-dev ts-jest @types/jest
# Additional useful packages
npm install --save-dev supertest @types/supertest # For API testing
npm install --save-dev jest-watch-typeahead # Enhanced watch mode
```
**Step 2: Create Jest Configuration**
Create `jest.config.js` with the following production-ready configuration:
```javascript
/** @type {import('jest').Config} */
module.exports = {
// Use ts-jest preset for TypeScript support
preset: 'ts-jest',
// Test environment
testEnvironment: 'node',
// Roots for test discovery
roots: ['<rootDir>/src', '<rootDir>/tests'],
// Test file patterns
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
// Transform files
transform: {
'^.+\\.ts$': [
'ts-jest',
{
tsconfig: {
target: 'es2020',
module: 'commonjs',
esModuleInterop: true,
allowSyntheticDefaultImports: true,
skipLibCheck: true,
strict: false,
noImplicitAny: false,
},
},
],
'^.+\\.js$': [
'ts-jest',
{
useESM: false,
tsconfig: {
target: 'es2020',
module: 'commonjs',
esModuleInterop: true,
allowSyntheticDefaultImports: true,
allowJs: true,
},
},
],
},
// Module file extensions
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
// Transform ignore patterns - adjust for ES modules
transformIgnorePatterns: ['node_modules/(?!(your-es-module-deps|.*\\.mjs$))'],
// Coverage configuration
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: [
'text', // Console output
'text-summary', // Brief summary
'lcov', // For IDE integration
'html', // Detailed HTML report
],
// Files to collect coverage from
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
'!src/**/index.ts', // Often just exports
'!src/generated/**', // Generated code
'!src/config/database.ts', // Database config (tested via integration)
],
// Coverage thresholds - TaskMaster standards
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80,
},
// Higher standards for critical business logic
'./src/utils/': {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
'./src/middleware/': {
branches: 80,
functions: 85,
lines: 85,
statements: 85,
},
},
// Setup files
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
// Global teardown to prevent worker process leaks
globalTeardown: '<rootDir>/tests/teardown.ts',
// Module path mapping (if needed)
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
// Clear mocks between tests
clearMocks: true,
// Restore mocks after each test
restoreMocks: true,
// Global test timeout
testTimeout: 10000,
// Projects for different test types
projects: [
// Unit tests - for pure functions only
{
displayName: 'unit',
testMatch: ['<rootDir>/src/**/*.test.ts'],
testPathIgnorePatterns: ['.*\\.integration\\.test\\.ts$', '/tests/'],
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
'!src/**/*.integration.test.ts',
],
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80,
},
},
},
// Integration tests - real database/services
{
displayName: 'integration',
testMatch: [
'<rootDir>/src/**/*.integration.test.ts',
'<rootDir>/tests/integration/**/*.test.ts',
],
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/tests/setup/integration.ts'],
testTimeout: 10000,
},
// E2E tests - full workflows
{
displayName: 'e2e',
testMatch: ['<rootDir>/tests/e2e/**/*.test.ts'],
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/tests/setup/e2e.ts'],
testTimeout: 30000,
},
],
// Verbose output for better debugging
verbose: true,
// Run projects sequentially to avoid conflicts
maxWorkers: 1,
// Enable watch mode plugins
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
};
```
**Step 3: Update package.json Scripts**
Add these scripts to your `package.json`:
```json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --selectProjects unit",
"test:integration": "jest --selectProjects integration",
"test:e2e": "jest --selectProjects e2e",
"test:ci": "jest --ci --coverage --watchAll=false"
}
}
```
**Step 4: Create Test Setup Files**
Create essential test setup files:
```typescript
// tests/setup.ts - Global setup
import { jest } from '@jest/globals';
// Global test configuration
beforeAll(() => {
// Set test timeout
jest.setTimeout(10000);
});
afterEach(() => {
// Clean up mocks after each test
jest.clearAllMocks();
});
```
```typescript
// tests/setup/integration.ts - Integration test setup
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
beforeAll(async () => {
// Connect to test database
await prisma.$connect();
});
afterAll(async () => {
// Cleanup and disconnect
await prisma.$disconnect();
});
beforeEach(async () => {
// Clean test data before each test
// Add your cleanup logic here
});
```
```typescript
// tests/teardown.ts - Global teardown
export default async () => {
// Global cleanup after all tests
console.log('Global test teardown complete');
};
```
**Step 5: Create Initial Test Structure**
```bash
# Create test directories
mkdir -p tests/{setup,fixtures,unit,integration,e2e}
mkdir -p tests/unit/src/{utils,services,middleware}
# Create sample test fixtures
mkdir tests/fixtures
```
### **Generic Testing Framework Setup (Any Language)**
#### **Framework Selection Guide**
**Python Projects:**
- **pytest**: Recommended for most Python projects
- **unittest**: Built-in, suitable for simple projects
- **Coverage**: Use `coverage.py` for code coverage
```bash
# Python setup example
pip install pytest pytest-cov
echo "[tool:pytest]" > pytest.ini
echo "testpaths = tests" >> pytest.ini
echo "addopts = --cov=src --cov-report=html --cov-report=term" >> pytest.ini
```
**Go Projects:**
- **Built-in testing**: Use Go's built-in `testing` package
- **Coverage**: Built-in with `go test -cover`
```bash
# Go setup example
go mod init your-project
mkdir -p tests
# Tests are typically *_test.go files alongside source
```
**Rust Projects:**
- **Built-in testing**: Use Rust's built-in test framework
- **cargo-tarpaulin**: For coverage analysis
```bash
# Rust setup example
cargo new your-project
cd your-project
cargo install cargo-tarpaulin # For coverage
```
**Java Projects:**
- **JUnit 5**: Modern testing framework
- **Maven/Gradle**: Build tools with testing integration
```xml
<!-- Maven pom.xml example -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
```
#### **Universal Testing Principles**
**Coverage Standards (Adapt to Your Language):**
- **Global Minimum**: 70-80% line coverage
- **Critical Code**: 85-90% coverage
- **New Features**: Must meet or exceed standards
- **Legacy Code**: Gradual improvement strategy
**Test Organization:**
- **Unit Tests**: Fast, isolated, no external dependencies
- **Integration Tests**: Test component interactions
- **E2E Tests**: Test complete user workflows
- **Performance Tests**: Load and stress testing (if applicable)
**Naming Conventions:**
- **Test Files**: `*.test.*`, `*_test.*`, or language-specific patterns
- **Test Functions**: Descriptive names (e.g., `should_return_error_for_invalid_input`)
- **Test Directories**: Organized by test type and mirroring source structure
#### **TaskMaster Integration for Any Framework**
**Document Testing Setup in Subtasks:**
```bash
# Update subtask with testing framework setup
task-master update-subtask --id=X.Y --prompt="Testing framework setup:
- Installed [Framework Name] with coverage support
- Configured [Coverage Tool] with thresholds: 80% lines, 70% branches
- Created test directory structure: unit/, integration/, e2e/
- Added test scripts to build configuration
- All setup tests passing"
```
**Testing Framework Verification:**
```bash
# Verify setup works
[test-command] # e.g., npm test, pytest, go test, cargo test
# Check coverage reporting
[coverage-command] # e.g., npm run test:coverage
# Update task with verification
task-master update-subtask --id=X.Y --prompt="Testing framework verified:
- Sample tests running successfully
- Coverage reporting functional
- CI/CD integration ready
- Ready to begin TDD workflow"
```
## **Test-Driven Development (TDD) Integration**
### **Core TDD Cycle with Jest**
```bash
# 1. Start development with watch mode
npm run test:watch
# 2. Write failing test first
# Create test file: src/utils/newFeature.test.ts
# Write test that describes expected behavior
# 3. Implement minimum code to make test pass
# 4. Refactor while keeping tests green
# 5. Add edge cases and error scenarios
```
### **TDD Workflow Per Subtask**
```bash
# When starting a new subtask:
task-master set-status --id=4.1 --status=in-progress
# Begin TDD cycle:
npm run test:watch # Keep running during development
# Document TDD progress in subtask:
task-master update-subtask --id=4.1 --prompt="TDD Progress:
- Written 3 failing tests for core functionality
- Implemented basic feature, tests now passing
- Adding edge case tests for error handling"
# Complete subtask with test summary:
task-master update-subtask --id=4.1 --prompt="Implementation complete:
- Feature implemented with 8 unit tests
- Coverage: 95% statements, 88% branches
- All tests passing, TDD cycle complete"
```
## **Testing Commands & Usage**
### **Development Commands**
```bash
# Primary development command - use during coding
npm run test:watch # Watch mode with Jest
npm run test:watch -- --testNamePattern="auth" # Watch specific tests
# Targeted testing during development
npm run test:unit # Run only unit tests
npm run test:unit -- --coverage # Unit tests with coverage
# Integration testing when APIs are ready
npm run test:integration # Run integration tests
npm run test:integration -- --detectOpenHandles # Debug hanging tests
# End-to-end testing for workflows
npm run test:e2e # Run E2E tests
npm run test:e2e -- --timeout=30000 # Extended timeout for E2E
```
### **Quality Assurance Commands**
```bash
# Full test suite with coverage (before commits)
npm run test:coverage # Complete coverage analysis
# All tests (CI/CD pipeline)
npm test # Run all test projects
# Specific test file execution
npm test -- auth.test.ts # Run specific test file
npm test -- --testNamePattern="should handle errors" # Run specific tests
```
## **Test Implementation Patterns**
### **Unit Test Development**
```typescript
// ✅ DO: Follow established patterns from auth.test.ts
describe('FeatureName', () => {
beforeEach(() => {
jest.clearAllMocks();
// Setup mocks with proper typing
});
describe('functionName', () => {
it('should handle normal case', () => {
// Test implementation with specific assertions
});
it('should throw error for invalid input', async () => {
// Error scenario testing
await expect(functionName(invalidInput))
.rejects.toThrow('Specific error message');
});
});
});
```
### **Integration Test Development**
```typescript
// ✅ DO: Use supertest for API endpoint testing
import request from 'supertest';
import { app } from '../../src/app';
describe('POST /api/auth/register', () => {
beforeEach(async () => {
await integrationTestUtils.cleanupTestData();
});
it('should register user successfully', async () => {
const userData = createTestUser();
const response = await request(app)
.post('/api/auth/register')
.send(userData)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
email: userData.email
});
// Verify database state
const user = await prisma.user.findUnique({
where: { email: userData.email }
});
expect(user).toBeTruthy();
});
});
```
### **E2E Test Development**
```typescript
// ✅ DO: Test complete user workflows
describe('User Authentication Flow', () => {
it('should complete registration → login → protected access', async () => {
// Step 1: Register
const userData = createTestUser();
await request(app)
.post('/api/auth/register')
.send(userData)
.expect(201);
// Step 2: Login
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: userData.email, password: userData.password })
.expect(200);
const { token } = loginResponse.body;
// Step 3: Access protected resource
await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
}, 30000); // Extended timeout for E2E
});
```
## **Mocking & Test Utilities**
### **Established Mocking Patterns**
```typescript
// ✅ DO: Use established bcrypt mocking pattern
jest.mock('bcrypt');
import bcrypt from 'bcrypt';
const mockHash = bcrypt.hash as jest.MockedFunction<typeof bcrypt.hash>;
const mockCompare = bcrypt.compare as jest.MockedFunction<typeof bcrypt.compare>;
// ✅ DO: Use Prisma mocking for unit tests
jest.mock('@prisma/client', () => ({
PrismaClient: jest.fn().mockImplementation(() => ({
user: {
create: jest.fn(),
findUnique: jest.fn(),
},
$connect: jest.fn(),
$disconnect: jest.fn(),
})),
}));
```
### **Test Fixtures Usage**
```typescript
// ✅ DO: Use centralized test fixtures
import { createTestUser, adminUser, invalidUser } from '../fixtures/users';
describe('User Service', () => {
it('should handle admin user creation', async () => {
const userData = createTestUser(adminUser);
// Test implementation
});
it('should reject invalid user data', async () => {
const userData = createTestUser(invalidUser);
// Error testing
});
});
```
## **Coverage Standards & Monitoring**
### **Coverage Thresholds**
- **Global Standards**: 80% lines/functions, 70% branches
- **Critical Code**: 90% utils, 85% middleware
- **New Features**: Must meet or exceed global thresholds
- **Legacy Code**: Gradual improvement with each change
### **Coverage Reporting & Analysis**
```bash
# Generate coverage reports
npm run test:coverage
# View detailed HTML report
open coverage/lcov-report/index.html
# Coverage files generated:
# - coverage/lcov-report/index.html # Detailed HTML report
# - coverage/lcov.info # LCOV format for IDE integration
# - coverage/coverage-final.json # JSON format for tooling
```
### **Coverage Quality Checks**
```typescript
// ✅ DO: Test all code paths
describe('validateInput', () => {
it('should return true for valid input', () => {
expect(validateInput('valid')).toBe(true);
});
it('should return false for various invalid inputs', () => {
expect(validateInput('')).toBe(false); // Empty string
expect(validateInput(null)).toBe(false); // Null value
expect(validateInput(undefined)).toBe(false); // Undefined
});
it('should throw for unexpected input types', () => {
expect(() => validateInput(123)).toThrow('Invalid input type');
});
});
```
## **Testing During Development Phases**
### **Feature Development Phase**
```bash
# 1. Start feature development
task-master set-status --id=X.Y --status=in-progress
# 2. Begin TDD cycle
npm run test:watch
# 3. Document test progress in subtask
task-master update-subtask --id=X.Y --prompt="Test development:
- Created test file with 5 failing tests
- Implemented core functionality
- Tests passing, adding error scenarios"
# 4. Verify coverage before completion
npm run test:coverage
# 5. Update subtask with final test status
task-master update-subtask --id=X.Y --prompt="Testing complete:
- 12 unit tests with full coverage
- All edge cases and error scenarios covered
- Ready for integration testing"
```
### **Integration Testing Phase**
```bash
# After API endpoints are implemented
npm run test:integration
# Update integration test templates
# Replace placeholder tests with real endpoint calls
# Document integration test results
task-master update-subtask --id=X.Y --prompt="Integration tests:
- Updated auth endpoint tests
- Database integration verified
- All HTTP status codes and responses tested"
```
### **Pre-Commit Testing Phase**
```bash
# Before committing code
npm run test:coverage # Verify all tests pass with coverage
npm run test:unit # Quick unit test verification
npm run test:integration # Integration test verification (if applicable)
# Commit pattern for test updates
git add tests/ src/**/*.test.ts
git commit -m "test(task-X): Add comprehensive tests for Feature Y
- Unit tests with 95% coverage (exceeds 90% threshold)
- Integration tests for API endpoints
- Test fixtures for data generation
- Proper mocking patterns established
Task X: Feature Y - Testing complete"
```
## **Error Handling & Debugging**
### **Test Debugging Techniques**
```typescript
// ✅ DO: Use test utilities for debugging
import { testUtils } from '../setup';
it('should debug complex operation', () => {
testUtils.withConsole(() => {
// Console output visible only for this test
console.log('Debug info:', complexData);
service.complexOperation();
});
});
// ✅ DO: Use proper async debugging
it('should handle async operations', async () => {
const promise = service.asyncOperation();
// Test intermediate state
expect(service.isProcessing()).toBe(true);
const result = await promise;
expect(result).toBe('expected');
expect(service.isProcessing()).toBe(false);
});
```
### **Common Test Issues & Solutions**
```bash
# Hanging tests (common with database connections)
npm run test:integration -- --detectOpenHandles
# Memory leaks in tests
npm run test:unit -- --logHeapUsage
# Slow tests identification
npm run test:coverage -- --verbose
# Mock not working properly
# Check: mock is declared before imports
# Check: jest.clearAllMocks() in beforeEach
# Check: TypeScript typing is correct
```
## **Continuous Integration Integration**
### **CI/CD Pipeline Testing**
```yaml
# Example GitHub Actions integration
- name: Run tests
run: |
npm ci
npm run test:coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
```
### **Pre-commit Hooks**
```bash
# Setup pre-commit testing (recommended)
# In package.json scripts:
"pre-commit": "npm run test:unit && npm run test:integration"
# Husky integration example:
npx husky add .husky/pre-commit "npm run test:unit"
```
## **Test Maintenance & Evolution**
### **Adding Tests for New Features**
1. **Create test file** alongside source code or in `tests/unit/`
2. **Follow established patterns** from `src/utils/auth.test.ts`
3. **Use existing fixtures** from `tests/fixtures/`
4. **Apply proper mocking** patterns for dependencies
5. **Meet coverage thresholds** for the module
### **Updating Integration/E2E Tests**
1. **Update templates** in `tests/integration/` when APIs change
2. **Modify E2E workflows** in `tests/e2e/` for new user journeys
3. **Update test fixtures** for new data requirements
4. **Maintain database cleanup** utilities
### **Test Performance Optimization**
- **Parallel execution**: Jest runs tests in parallel by default
- **Test isolation**: Use proper setup/teardown for independence
- **Mock optimization**: Mock heavy dependencies appropriately
- **Database efficiency**: Use transaction rollbacks where possible
---
**Key References:**
- [Testing Standards](mdc:.cursor/rules/tests.mdc)
- [Git Workflow](mdc:.cursor/rules/git_workflow.mdc)
- [Development Workflow](mdc:.cursor/rules/dev_workflow.mdc)
- [Jest Configuration](mdc:jest.config.js)

45
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,45 @@
# What type of PR is this?
<!-- Check one -->
- [ ] 🐛 Bug fix
- [ ] ✨ Feature
- [ ] 🔌 Integration
- [ ] 📝 Docs
- [ ] 🧹 Refactor
- [ ] Other:
## Description
<!-- What does this PR do? -->
## Related Issues
<!-- Link issues: Fixes #123 -->
## How to Test This
<!-- Quick steps to verify the changes work -->
```bash
# Example commands or steps
```
**Expected result:**
<!-- What should happen? -->
## Contributor Checklist
- [ ] Created changeset: `npm run changeset`
- [ ] Tests pass: `npm test`
- [ ] Format check passes: `npm run format-check` (or `npm run format` to fix)
- [ ] Addressed CodeRabbit comments (if any)
- [ ] Linked related issues (if any)
- [ ] Manually tested the changes
## Changelog Entry
<!-- One line describing the change for users -->
<!-- Example: "Added Kiro IDE integration with automatic task status updates" -->
---
### For Maintainers
- [ ] PR title follows conventional commits
- [ ] Target branch correct
- [ ] Labels added
- [ ] Milestone assigned (if applicable)

39
.github/PULL_REQUEST_TEMPLATE/bugfix.md vendored Normal file
View File

@@ -0,0 +1,39 @@
## 🐛 Bug Fix
### 🔍 Bug Description
<!-- Describe the bug -->
### 🔗 Related Issues
<!-- Fixes #123 -->
### ✨ Solution
<!-- How does this PR fix the bug? -->
## How to Test
### Steps that caused the bug:
1.
2.
**Before fix:**
**After fix:**
### Quick verification:
```bash
# Commands to verify the fix
```
## Contributor Checklist
- [ ] Created changeset: `npm run changeset`
- [ ] Tests pass: `npm test`
- [ ] Format check passes: `npm run format-check`
- [ ] Addressed CodeRabbit comments
- [ ] Added unit tests (if applicable)
- [ ] Manually verified the fix works
---
### For Maintainers
- [ ] Root cause identified
- [ ] Fix doesn't introduce new issues
- [ ] CI passes

View File

@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: 🐛 Bug Fix
url: https://github.com/eyaltoledano/claude-task-master/compare/next...HEAD?template=bugfix.md
about: Fix a bug in Task Master
- name: ✨ New Feature
url: https://github.com/eyaltoledano/claude-task-master/compare/next...HEAD?template=feature.md
about: Add a new feature to Task Master
- name: 🔌 New Integration
url: https://github.com/eyaltoledano/claude-task-master/compare/next...HEAD?template=integration.md
about: Add support for a new tool, IDE, or platform

View File

@@ -0,0 +1,49 @@
## ✨ New Feature
### 📋 Feature Description
<!-- Brief description -->
### 🎯 Problem Statement
<!-- What problem does this feature solve? Why is it needed? -->
### 💡 Solution
<!-- How does this feature solve the problem? What's the approach? -->
### 🔗 Related Issues
<!-- Link related issues: Fixes #123, Part of #456 -->
## How to Use It
### Quick Start
```bash
# Basic usage example
```
### Example
<!-- Show a real use case -->
```bash
# Practical example
```
**What you should see:**
<!-- Expected behavior -->
## Contributor Checklist
- [ ] Created changeset: `npm run changeset`
- [ ] Tests pass: `npm test`
- [ ] Format check passes: `npm run format-check`
- [ ] Addressed CodeRabbit comments
- [ ] Added tests for new functionality
- [ ] Manually tested in CLI mode
- [ ] Manually tested in MCP mode (if applicable)
## Changelog Entry
<!-- One-liner for release notes -->
---
### For Maintainers
- [ ] Feature aligns with project vision
- [ ] CIs pass
- [ ] Changeset file exists

View File

@@ -0,0 +1,53 @@
# 🔌 New Integration
## What tool/IDE is being integrated?
<!-- Name and brief description -->
## What can users do with it?
<!-- Key benefits -->
## How to Enable
### Setup
```bash
task-master rules add [name]
# Any other setup steps
```
### Example Usage
<!-- Show it in action -->
```bash
# Real example
```
### Natural Language Hooks (if applicable)
```
"When tests pass, mark task as done"
# Other examples
```
## Contributor Checklist
- [ ] Created changeset: `npm run changeset`
- [ ] Tests pass: `npm test`
- [ ] Format check passes: `npm run format-check`
- [ ] Addressed CodeRabbit comments
- [ ] Integration fully tested with target tool/IDE
- [ ] Error scenarios tested
- [ ] Added integration tests
- [ ] Documentation includes setup guide
- [ ] Examples are working and clear
---
## For Maintainers
- [ ] Integration stability verified
- [ ] Documentation comprehensive
- [ ] Examples working

21
.github/scripts/release.sh vendored Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -e
echo "🚀 Starting release process..."
# Double-check we're not in pre-release mode (safety net)
if [ -f .changeset/pre.json ]; then
echo "⚠️ Warning: pre.json still exists. Removing it..."
rm -f .changeset/pre.json
fi
# Check if the extension version has changed and tag it
# This prevents changeset from trying to publish the private package
node .github/scripts/tag-extension.mjs
# Run changeset publish for npm packages
npx changeset publish
echo "✅ Release process completed!"
# The extension tag (if created) will trigger the extension-release workflow

99
.github/scripts/tag-extension.mjs vendored Normal file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { readFileSync, existsSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Find the root directory by looking for package.json
function findRootDir(startDir) {
let currentDir = resolve(startDir);
while (currentDir !== '/') {
if (existsSync(join(currentDir, 'package.json'))) {
// Verify it's the root package.json by checking for expected fields
try {
const pkg = JSON.parse(
readFileSync(join(currentDir, 'package.json'), 'utf8')
);
if (pkg.name === 'task-master-ai' || pkg.repository) {
return currentDir;
}
} catch {}
}
currentDir = dirname(currentDir);
}
throw new Error('Could not find root directory');
}
const rootDir = findRootDir(__dirname);
// Read the extension's package.json
const extensionDir = join(rootDir, 'apps', 'extension');
const pkgPath = join(extensionDir, 'package.json');
let pkg;
try {
const pkgContent = readFileSync(pkgPath, 'utf8');
pkg = JSON.parse(pkgContent);
} catch (error) {
console.error('Failed to read package.json:', error.message);
process.exit(1);
}
// Read root package.json for repository info
const rootPkgPath = join(rootDir, 'package.json');
let rootPkg;
try {
const rootPkgContent = readFileSync(rootPkgPath, 'utf8');
rootPkg = JSON.parse(rootPkgContent);
} catch (error) {
console.error('Failed to read root package.json:', error.message);
process.exit(1);
}
// Ensure we have required fields
assert(pkg.name, 'package.json must have a name field');
assert(pkg.version, 'package.json must have a version field');
assert(rootPkg.repository, 'root package.json must have a repository field');
const tag = `${pkg.name}@${pkg.version}`;
// Get repository URL from root package.json
const repoUrl = rootPkg.repository.url;
const { status, stdout, error } = spawnSync('git', ['ls-remote', repoUrl, tag]);
assert.equal(status, 0, error);
const exists = String(stdout).trim() !== '';
if (!exists) {
console.log(`Creating new extension tag: ${tag}`);
// Create the tag
const tagResult = spawnSync('git', ['tag', tag]);
if (tagResult.status !== 0) {
console.error(
'Failed to create tag:',
tagResult.error || tagResult.stderr.toString()
);
process.exit(1);
}
// Push the tag
const pushResult = spawnSync('git', ['push', 'origin', tag]);
if (pushResult.status !== 0) {
console.error(
'Failed to push tag:',
pushResult.error || pushResult.stderr.toString()
);
process.exit(1);
}
console.log(`✅ Successfully created and pushed tag: ${tag}`);
} else {
console.log(`Extension tag already exists: ${tag}`);
}

143
.github/workflows/extension-ci.yml vendored Normal file
View File

@@ -0,0 +1,143 @@
name: Extension CI
on:
push:
branches:
- main
- next
paths:
- 'apps/extension/**'
- '.github/workflows/extension-ci.yml'
pull_request:
branches:
- main
- next
paths:
- 'apps/extension/**'
- '.github/workflows/extension-ci.yml'
permissions:
contents: read
jobs:
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Extension Dependencies
working-directory: apps/extension
run: npm ci
timeout-minutes: 5
typecheck:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Restore node_modules
uses: actions/cache@v4
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install if cache miss
working-directory: apps/extension
run: npm ci
timeout-minutes: 3
- name: Type Check Extension
working-directory: apps/extension
run: npm run check-types
env:
FORCE_COLOR: 1
build:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Restore node_modules
uses: actions/cache@v4
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install if cache miss
working-directory: apps/extension
run: npm ci
timeout-minutes: 3
- name: Build Extension
working-directory: apps/extension
run: npm run build
env:
FORCE_COLOR: 1
- name: Package Extension
working-directory: apps/extension
run: npm run package
env:
FORCE_COLOR: 1
- name: Verify Package Contents
working-directory: apps/extension
run: |
echo "Checking vsix-build contents..."
ls -la vsix-build/
echo "Checking dist contents..."
ls -la vsix-build/dist/
echo "Checking package.json exists..."
test -f vsix-build/package.json
- name: Create VSIX Package (Test)
working-directory: apps/extension/vsix-build
run: npx vsce package --no-dependencies
env:
FORCE_COLOR: 1
- name: Upload Extension Artifact
uses: actions/upload-artifact@v4
with:
name: extension-package
path: |
apps/extension/vsix-build/*.vsix
apps/extension/dist/
retention-days: 30

137
.github/workflows/extension-release.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
name: Extension Release
on:
push:
tags:
- "extension@*"
permissions:
contents: write
concurrency: extension-release-${{ github.ref }}
jobs:
publish-extension:
runs-on: ubuntu-latest
environment: extension-release
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Extension Dependencies
working-directory: apps/extension
run: npm ci
timeout-minutes: 5
- name: Type Check Extension
working-directory: apps/extension
run: npm run check-types
env:
FORCE_COLOR: 1
- name: Build Extension
working-directory: apps/extension
run: npm run build
env:
FORCE_COLOR: 1
- name: Package Extension
working-directory: apps/extension
run: npm run package
env:
FORCE_COLOR: 1
- name: Create VSIX Package
working-directory: apps/extension/vsix-build
run: npx vsce package --no-dependencies
env:
FORCE_COLOR: 1
- name: Get VSIX filename
id: vsix-info
working-directory: apps/extension/vsix-build
run: |
VSIX_FILE=$(find . -maxdepth 1 -name "*.vsix" -type f | head -n1 | xargs basename)
if [ -z "$VSIX_FILE" ]; then
echo "Error: No VSIX file found"
exit 1
fi
echo "vsix-filename=$VSIX_FILE" >> "$GITHUB_OUTPUT"
echo "Found VSIX: $VSIX_FILE"
- name: Publish to VS Code Marketplace
working-directory: apps/extension/vsix-build
run: npx vsce publish --packagePath "${{ steps.vsix-info.outputs.vsix-filename }}"
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
FORCE_COLOR: 1
- name: Install Open VSX CLI
run: npm install -g ovsx
- name: Publish to Open VSX Registry
working-directory: apps/extension/vsix-build
run: ovsx publish "${{ steps.vsix-info.outputs.vsix-filename }}"
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
FORCE_COLOR: 1
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Extension ${{ github.ref_name }}
body: |
VS Code Extension Release ${{ github.ref_name }}
**Marketplaces:**
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Hamster.task-master-hamster)
- [Open VSX Registry](https://open-vsx.org/extension/Hamster/task-master-hamster)
draft: false
prerelease: false
- name: Upload VSIX to Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: apps/extension/vsix-build/${{ steps.vsix-info.outputs.vsix-filename }}
asset_name: ${{ steps.vsix-info.outputs.vsix-filename }}
asset_content_type: application/zip
- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: extension-release-${{ github.ref_name }}
path: |
apps/extension/vsix-build/*.vsix
apps/extension/dist/
retention-days: 90
notify-success:
needs: publish-extension
if: success()
runs-on: ubuntu-latest
steps:
- name: Success Notification
run: |
echo "🎉 Extension ${{ github.ref_name }} successfully published!"
echo "📦 Available on VS Code Marketplace"
echo "🌍 Available on Open VSX Registry"
echo "🏷️ GitHub release created: ${{ github.ref_name }}"

View File

@@ -16,7 +16,7 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'npm' cache: "npm"
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v4 uses: actions/cache@v4
@@ -32,10 +32,13 @@ jobs:
run: npm ci run: npm ci
timeout-minutes: 2 timeout-minutes: 2
- name: Enter RC mode - name: Enter RC mode (if not already in RC mode)
run: | run: |
npx changeset pre exit || true # ensure were in the right pre-mode (tag "rc")
npx changeset pre enter rc if [ ! -f .changeset/pre.json ] \
|| [ "$(jq -r '.tag' .changeset/pre.json 2>/dev/null || echo '')" != "rc" ]; then
npx changeset pre enter rc
fi
- name: Version RC packages - name: Version RC packages
run: npx changeset version run: npx changeset version
@@ -51,12 +54,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Exit RC mode
run: npx changeset pre exit
- name: Commit & Push changes - name: Commit & Push changes
uses: actions-js/push@master uses: actions-js/push@master
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }} branch: ${{ github.ref }}
message: 'chore: rc version bump' message: "chore: rc version bump"

View File

@@ -6,6 +6,11 @@ on:
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
id-token: write
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -33,13 +38,31 @@ jobs:
run: npm ci run: npm ci
timeout-minutes: 2 timeout-minutes: 2
- name: Exit pre-release mode (safety check) - name: Exit pre-release mode and clean up
run: npx changeset pre exit || true run: |
echo "🔄 Ensuring we're not in pre-release mode for main branch..."
# Exit pre-release mode if we're in it
npx changeset pre exit || echo "Not in pre-release mode"
# Remove pre.json file if it exists (belt and suspenders approach)
if [ -f .changeset/pre.json ]; then
echo "🧹 Removing pre.json file..."
rm -f .changeset/pre.json
fi
# Verify the file is gone
if [ ! -f .changeset/pre.json ]; then
echo "✅ pre.json successfully removed"
else
echo "❌ Failed to remove pre.json"
exit 1
fi
- name: Create Release Pull Request or Publish to npm - name: Create Release Pull Request or Publish to npm
uses: changesets/action@v1 uses: changesets/action@v1
with: with:
publish: npm run release publish: ./.github/scripts/release.sh
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

7
.gitignore vendored
View File

@@ -87,3 +87,10 @@ dev-debug.log
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# VS Code extension test files
.vscode-test/
apps/extension/.vscode-test/
# apps/extension
apps/extension/vsix-build/

View File

@@ -1,3 +0,0 @@
Testing rules that you can help me improve to see how it works<!------------------------------------------------------------------------------------
Add Rules to this file or a short description and have Kiro refine them for you:
------------------------------------------------------------------------------------->

View File

@@ -0,0 +1,153 @@
---
title: Research Session
query: "do we need to add new commands or can we just weaponize the gh cli tool"
date: 2025-08-01
time: 2:27:52 p.m.
timestamp: 2025-08-01T11:27:52.249Z
exchanges: 3
---
# Research Session
## Initial Query
**Question:** do we need to add new commands or can we just weaponize the gh cli tool
**Response:**
For the implementation of the `task-master git` command suite to automate git workflows (Task 97), it is possible to leverage the existing **GitHub CLI (`gh`) tool** rather than creating entirely new git commands from scratch. The GitHub CLI provides a rich set of commands that cover many GitHub-related operations such as branch management, pull request handling, workflow triggering, and release management, which can be scripted and integrated into your CLI tool[1][2][3][4].
### Key Points Supporting Weaponizing GitHub CLI:
- **Comprehensive GitHub Operations**: The `gh` CLI supports creating branches, managing pull requests, releases, tags, and even triggering and monitoring GitHub Actions workflows. This aligns well with the requirements of automated branch management, commit generation, and PR description generation in Task 97[1][3].
- **Scripting and Automation Friendly**: GitHub CLI commands can be scripted and integrated into custom workflows or CLI tools. You can create aliases or wrap `gh` commands inside your own CLI commands to provide a seamless user experience while relying on the battle-tested GitHub CLI for underlying operations[1][3].
- **Authentication and Permissions**: `gh` uses environment variables like `GH_TOKEN` or `GITHUB_TOKEN` for authentication, which can be securely managed in CI/CD pipelines or local environments. This simplifies integration without needing to implement custom authentication logic[3][4].
- **Extensibility**: If some specific git workflow features are not directly supported by `gh`, you can extend it with custom scripts or combine it with native git commands. This hybrid approach reduces the need to reinvent core git operations while allowing customization[1].
### Considerations for Your Project:
- Since Task 97 requires **automated branch management** (branch creation, validation, switching, cleanup), **intelligent commit generation**, and **PR description generation**, you can implement these by invoking `gh` commands programmatically within your `task-master git` commands.
- For example, branch creation and switching can be done with native git commands wrapped in your CLI, while PR creation and management can be handled via `gh pr create`, `gh pr merge`, etc.
- You may still need to implement some **custom logic** for commit type detection, branch name validation, and PR description generation based on your task metadata, but the underlying git and GitHub operations can be delegated to `gh`.
- This approach aligns with your existing CLI foundation (Task 2) and leverages a well-supported tool, reducing development effort and increasing reliability.
### Summary
You **do not necessarily need to add entirely new git commands** for Task 97. Instead, you can **weaponize the GitHub CLI (`gh`) tool** by wrapping and scripting its commands within your `task-master git` command suite. This will allow you to automate git workflows efficiently, maintain consistency, and leverage GitHubs API capabilities without reinventing the wheel[1][2][3][4].
If there are gaps in `gh` functionality for your specific workflow needs, you can supplement with native git commands or custom scripts, but the core git and GitHub operations should be handled by `gh` for best results.
---
## Follow-up 1
**Question:** how does this relate to git worktrees? i hear that they get super complicated when trying to implement them.
**Response:**
Git worktrees allow you to have multiple working directories attached to the same Git repository, each checked out to different branches simultaneously. This enables parallel development workflows without the overhead of cloning the repository multiple times or constantly switching branches in a single working directory[2][3].
However, **implementing or managing git worktrees can get complicated** for several reasons:
- **Complexity for users and tooling:** Managing multiple active worktrees requires careful tracking of which directory corresponds to which branch. This can be confusing and error-prone, especially for novices or when many worktrees are in use simultaneously. Mistakes like committing to the wrong branch or working in the wrong directory are common risks[1].
- **Maintenance overhead:** Worktrees accumulate over time and need to be pruned or cleaned up regularly (`git worktree prune`) to avoid clutter and potential confusion. This adds an extra maintenance step to the workflow[1].
- **Syncing and update challenges:** Each worktree is a separate checkout and can become outdated if not regularly updated from the main repository. This can lead to merge conflicts or redundant work if branches diverge significantly[1].
- **Tooling and integration limitations:** Not all Git tools, scripts, or GUIs fully support worktrees or are optimized for them. This can cause inconsistencies or require additional custom tooling to manage worktrees effectively[1].
- **Performance considerations:** Managing multiple worktrees simultaneously can have performance implications, especially in very large repositories, due to the overhead of maintaining multiple checkouts and metadata[1].
In the context of your project, which involves automating git workflows and possibly wrapping or extending git/GitHub CLI commands, these complexities imply:
- If you consider supporting git worktrees in your `task-master git` command suite, you will need to handle the complexity of managing multiple directories and branches carefully.
- You might need to implement additional logic or tooling to track and manage worktrees, avoid user confusion, and ensure synchronization between them.
- Since your project already plans to leverage the GitHub CLI (`gh`) and native git commands, you could integrate worktree commands (`git worktree add`, `git worktree remove`, `git worktree list`) but should be prepared for the added complexity in workflow management and user documentation.
- Custom wrappers or managers (like the one described in [3]) can help reduce friction by automating worktree creation, switching, and cleanup, which might be a useful approach to consider.
Overall, **git worktrees provide powerful parallel branch management but introduce complexity in implementation and user experience** that must be carefully managed through tooling, automation, and clear documentation[1][3][4].
---
**Summary Table of Git Worktree Challenges and Considerations**
| Aspect | Description | Implication for Project |
|-------------------------|-----------------------------------------------------------------------------------------------|----------------------------------------------------------|
| User Complexity | Confusing multiple directories and branches | Need clear UI/UX and safeguards |
| Maintenance Overhead | Requires pruning and cleanup of stale worktrees | Automate cleanup or provide commands |
| Syncing Issues | Worktrees can become outdated or diverge | Implement update checks and sync workflows |
| Tooling Support | Limited support in some Git tools and GUIs | May require custom tooling or wrappers |
| Performance | Overhead in large repos with many worktrees | Monitor and optimize performance |
| Workflow Automation | Manual worktree management is tedious | Build custom managers or wrappers to simplify usage |
---
Given your projects focus on automating git workflows and leveraging existing CLI tools, **implementing git worktree support is feasible but will require careful design to handle these complexities effectively**. You may want to start with basic worktree commands and incrementally add automation and safeguards as needed.
---
## Follow-up 2
**Question:** since we already have a way to have separated task lists per branch, couldn't we do the same thing for worktrees? each taskmaster list (tag) would have its own worktree based on the name of the tag?
**Response:**
Yes, you can apply a similar approach used for separated task lists per branch to git worktrees by associating each taskmaster list (tag) with its own git worktree named after the tag. This means each tag would correspond to a dedicated worktree directory checked out to a branch or commit related to that tag, enabling isolated parallel work environments for each task list.
### How This Maps to Git Worktrees
- **Git worktrees** allow multiple working directories linked to the same repository, each checked out to a different branch or commit[1][2]. This lets you work on multiple branches simultaneously without switching contexts in a single directory.
- When you create a worktree with `git worktree add <path> [<branch>]`, Git creates a new linked working tree at `<path>`, optionally checking out a specified branch or creating a new one[1][2]. Naming the worktree directory after the tag (taskmaster list) is straightforward and semantically clear.
- This setup mirrors your existing branch-based task list separation but extends it to physical directories, which can improve workflow isolation and reduce context switching.
### Benefits for Your Project
- **Isolated environments per task list/tag:** Each tags worktree can have its own checked-out branch, allowing independent development, testing, or review without interference.
- **Parallel workflows:** Developers can switch between worktrees (directories) instead of switching branches in one directory, reducing errors and improving productivity.
- **Automation potential:** Your `task-master git` commands can automate creating, switching, and cleaning up these worktrees based on tags, similar to how you manage branches and task lists.
### Challenges and Considerations
- **Complexity in management:** Git worktrees add complexity in tracking multiple directories and ensuring users operate in the correct one[1]. Your CLI should provide clear commands and feedback to avoid confusion.
- **Maintenance overhead:** Worktrees need pruning and cleanup (`git worktree prune` or `git worktree remove`) to avoid clutter[1]. Automating cleanup in your tool would be beneficial.
- **Syncing and updates:** Each worktree is a separate checkout and can become outdated. Your automation should include commands to update or rebase worktrees as needed.
- **Tooling support:** Not all Git tools or scripts fully support worktrees, so your CLI wrappers should handle edge cases and provide a consistent experience[1][3].
### Practical Implementation Ideas
- When a new tag (taskmaster list) is created, run `git worktree add` to create a new worktree directory named after the tag.
- Store metadata linking tags to worktree paths in your taskmaster configuration to track and manage them.
- Provide commands like `task-master worktree list`, `task-master worktree add <tag>`, and `task-master worktree remove <tag>` to manage these worktrees.
- Integrate with GitHub CLI (`gh`) and native git commands to automate branch creation, PR handling, and syncing within each worktree.
- Consider inspiration from existing tools like [wt](https://github.com/taecontrol/wt) that automate worktree environment setup and teardown[3].
### Summary
Using git worktrees per taskmaster tag is a feasible and potentially powerful extension of your current branch-based task list separation. It offers isolated, parallel working directories aligned with your tags, improving workflow clarity and reducing branch-switching overhead. However, it requires careful tooling and automation to manage complexity, maintenance, and synchronization effectively. Given your projects focus on automating git workflows and leveraging CLI tools, implementing this feature incrementally with robust user feedback and cleanup commands would be a practical approach.
---
*Generated by Task Master Research Command*
*Timestamp: 2025-08-01T11:27:52.249Z*

View File

@@ -1,373 +1,21 @@
{ {
"meta": { "meta": {
"generatedAt": "2025-05-27T16:34:53.088Z", "generatedAt": "2025-08-02T14:28:59.851Z",
"tasksAnalyzed": 1, "tasksAnalyzed": 1,
"totalTasks": 84, "totalTasks": 93,
"analysisCount": 45, "analysisCount": 1,
"thresholdScore": 5, "thresholdScore": 5,
"projectName": "Taskmaster", "projectName": "Taskmaster",
"usedResearch": true "usedResearch": false
}, },
"complexityAnalysis": [ "complexityAnalysis": [
{ {
"taskId": 24, "taskId": 24,
"taskTitle": "Implement AI-Powered Test Generation Command", "taskTitle": "Implement AI-Powered Test Generation Command",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Break down the implementation of the AI-powered test generation command into detailed subtasks covering: command structure setup, AI prompt engineering, test file generation logic, integration with Claude API, and comprehensive error handling.",
"reasoning": "This task involves complex integration with an AI service (Claude), requires sophisticated prompt engineering, and needs to generate structured code files. The existing 3 subtasks are a good start but could be expanded to include more detailed steps for AI integration, error handling, and test file formatting."
},
{
"taskId": 26,
"taskTitle": "Implement Context Foundation for AI Operations",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "The current 4 subtasks for implementing the context foundation appear comprehensive. Consider if any additional subtasks are needed for testing, documentation, or integration with existing systems.",
"reasoning": "This task involves creating a foundation for context integration with several well-defined components. The existing 4 subtasks cover the main implementation areas (context-file flag, cursor rules integration, context extraction utility, and command handler updates). The complexity is moderate as it requires careful integration with existing systems but has clear requirements."
},
{
"taskId": 27,
"taskTitle": "Implement Context Enhancements for AI Operations",
"complexityScore": 7,
"recommendedSubtasks": 4,
"expansionPrompt": "The current 4 subtasks for implementing context enhancements appear well-structured. Consider if any additional subtasks are needed for testing, documentation, or performance optimization.",
"reasoning": "This task builds upon the foundation from Task #26 and adds more sophisticated context handling features. The 4 existing subtasks cover the main implementation areas (code context extraction, task history context, PRD context integration, and context formatting). The complexity is higher than the foundation task due to the need for intelligent context selection and optimization."
},
{
"taskId": 28,
"taskTitle": "Implement Advanced ContextManager System",
"complexityScore": 8,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing the advanced ContextManager system appear comprehensive. Consider if any additional subtasks are needed for testing, documentation, or backward compatibility with previous context implementations.",
"reasoning": "This task represents the most complex phase of the context implementation, requiring a sophisticated class design, optimization algorithms, and integration with multiple systems. The 5 existing subtasks cover the core implementation areas, but the complexity is high due to the need for intelligent context prioritization, token management, and performance monitoring."
},
{
"taskId": 40,
"taskTitle": "Implement 'plan' Command for Task Implementation Planning",
"complexityScore": 5,
"recommendedSubtasks": 4,
"expansionPrompt": "The current 4 subtasks for implementing the 'plan' command appear well-structured. Consider if any additional subtasks are needed for testing, documentation, or integration with existing task management workflows.",
"reasoning": "This task involves creating a new command that leverages AI to generate implementation plans. The existing 4 subtasks cover the main implementation areas (retrieving task content, generating plans with AI, formatting in XML, and error handling). The complexity is moderate as it builds on existing patterns for task updates but requires careful AI integration."
},
{
"taskId": 41,
"taskTitle": "Implement Visual Task Dependency Graph in Terminal",
"complexityScore": 8,
"recommendedSubtasks": 10,
"expansionPrompt": "The current 10 subtasks for implementing the visual task dependency graph appear comprehensive. Consider if any additional subtasks are needed for performance optimization with large graphs or additional visualization options.",
"reasoning": "This task involves creating a sophisticated visualization system for terminal display, which is inherently complex due to layout algorithms, ASCII/Unicode rendering, and handling complex dependency relationships. The 10 existing subtasks cover all major aspects of implementation, from CLI interface to accessibility features."
},
{
"taskId": 42,
"taskTitle": "Implement MCP-to-MCP Communication Protocol",
"complexityScore": 9,
"recommendedSubtasks": 8,
"expansionPrompt": "The current 8 subtasks for implementing the MCP-to-MCP communication protocol appear well-structured. Consider if any additional subtasks are needed for security hardening, performance optimization, or comprehensive documentation.",
"reasoning": "This task involves designing and implementing a complex communication protocol between different MCP tools and servers. It requires sophisticated adapter patterns, client-server architecture, and handling of multiple operational modes. The complexity is very high due to the need for standardization, security, and backward compatibility."
},
{
"taskId": 44,
"taskTitle": "Implement Task Automation with Webhooks and Event Triggers",
"complexityScore": 8,
"recommendedSubtasks": 7,
"expansionPrompt": "The current 7 subtasks for implementing task automation with webhooks appear comprehensive. Consider if any additional subtasks are needed for security testing, rate limiting implementation, or webhook monitoring tools.",
"reasoning": "This task involves creating a sophisticated event system with webhooks for integration with external services. The complexity is high due to the need for secure authentication, reliable delivery mechanisms, and handling of various webhook formats and protocols. The existing subtasks cover the main implementation areas but security and monitoring could be emphasized more."
},
{
"taskId": 45,
"taskTitle": "Implement GitHub Issue Import Feature",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing the GitHub issue import feature appear well-structured. Consider if any additional subtasks are needed for handling GitHub API rate limiting, caching, or supporting additional issue metadata.",
"reasoning": "This task involves integrating with the GitHub API to import issues as tasks. The complexity is moderate as it requires API authentication, data mapping, and error handling. The existing 5 subtasks cover the main implementation areas from design to end-to-end implementation."
},
{
"taskId": 46,
"taskTitle": "Implement ICE Analysis Command for Task Prioritization",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing the ICE analysis command appear comprehensive. Consider if any additional subtasks are needed for visualization of ICE scores or integration with other prioritization methods.",
"reasoning": "This task involves creating an AI-powered analysis system for task prioritization using the ICE methodology. The complexity is high due to the need for sophisticated scoring algorithms, AI integration, and report generation. The existing subtasks cover the main implementation areas from algorithm design to integration with existing systems."
},
{
"taskId": 47,
"taskTitle": "Enhance Task Suggestion Actions Card Workflow",
"complexityScore": 6,
"recommendedSubtasks": 6,
"expansionPrompt": "The current 6 subtasks for enhancing the task suggestion actions card workflow appear well-structured. Consider if any additional subtasks are needed for user testing, accessibility improvements, or performance optimization.",
"reasoning": "This task involves redesigning the UI workflow for task expansion and management. The complexity is moderate as it requires careful UX design and state management but builds on existing components. The 6 existing subtasks cover the main implementation areas from design to testing."
},
{
"taskId": 48,
"taskTitle": "Refactor Prompts into Centralized Structure",
"complexityScore": 4,
"recommendedSubtasks": 3,
"expansionPrompt": "The current 3 subtasks for refactoring prompts into a centralized structure appear appropriate. Consider if any additional subtasks are needed for prompt versioning, documentation, or testing.",
"reasoning": "This task involves a straightforward refactoring to improve code organization. The complexity is relatively low as it primarily involves moving code rather than creating new functionality. The 3 existing subtasks cover the main implementation areas from directory structure to integration."
},
{
"taskId": 49,
"taskTitle": "Implement Code Quality Analysis Command",
"complexityScore": 8, "complexityScore": 8,
"recommendedSubtasks": 6, "recommendedSubtasks": 6,
"expansionPrompt": "The current 6 subtasks for implementing the code quality analysis command appear comprehensive. Consider if any additional subtasks are needed for performance optimization with large codebases or integration with existing code quality tools.", "expansionPrompt": "Expand task 24 'Implement AI-Powered Test Generation Command' into 6 subtasks, focusing on: 1) Command structure implementation, 2) AI prompt engineering for test generation, 3) Test file generation and output, 4) Framework-specific template implementation, 5) MCP tool integration, and 6) Documentation and help system integration. Include detailed implementation steps, dependencies, and testing approaches for each subtask.",
"reasoning": "This task involves creating a sophisticated code analysis system with pattern recognition, best practice verification, and AI-powered recommendations. The complexity is high due to the need for code parsing, complex analysis algorithms, and integration with AI services. The existing subtasks cover the main implementation areas from algorithm design to user interface." "reasoning": "This task has high complexity due to several challenging aspects: 1) AI integration requiring sophisticated prompt engineering, 2) Test generation across multiple frameworks, 3) File system operations with proper error handling, 4) MCP tool integration, 5) Complex configuration requirements, and 6) Framework-specific template generation. The task already has 5 subtasks but could benefit from reorganization based on the updated implementation details in the info blocks, particularly around framework support and configuration."
},
{
"taskId": 50,
"taskTitle": "Implement Test Coverage Tracking System by Task",
"complexityScore": 9,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing the test coverage tracking system appear well-structured. Consider if any additional subtasks are needed for integration with CI/CD systems, performance optimization, or visualization tools.",
"reasoning": "This task involves creating a complex system that maps test coverage to specific tasks and subtasks. The complexity is very high due to the need for sophisticated data structures, integration with coverage tools, and AI-powered test generation. The existing subtasks are comprehensive and cover the main implementation areas from data structure design to AI integration."
},
{
"taskId": 51,
"taskTitle": "Implement Perplexity Research Command",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing the Perplexity research command appear comprehensive. Consider if any additional subtasks are needed for caching optimization, result formatting, or integration with other research tools.",
"reasoning": "This task involves creating a new command that integrates with the Perplexity AI API for research. The complexity is moderate as it requires API integration, context extraction, and result formatting. The 5 existing subtasks cover the main implementation areas from API client to caching system."
},
{
"taskId": 52,
"taskTitle": "Implement Task Suggestion Command for CLI",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing the task suggestion command appear well-structured. Consider if any additional subtasks are needed for suggestion quality evaluation, user feedback collection, or integration with existing task workflows.",
"reasoning": "This task involves creating a new CLI command that generates contextually relevant task suggestions using AI. The complexity is moderate as it requires AI integration, context collection, and interactive CLI interfaces. The existing subtasks cover the main implementation areas from data collection to user interface."
},
{
"taskId": 53,
"taskTitle": "Implement Subtask Suggestion Feature for Parent Tasks",
"complexityScore": 6,
"recommendedSubtasks": 6,
"expansionPrompt": "The current 6 subtasks for implementing the subtask suggestion feature appear comprehensive. Consider if any additional subtasks are needed for suggestion quality metrics, user feedback collection, or performance optimization.",
"reasoning": "This task involves creating a feature that suggests contextually relevant subtasks for parent tasks. The complexity is moderate as it builds on existing task management systems but requires sophisticated AI integration and context analysis. The 6 existing subtasks cover the main implementation areas from validation to testing."
},
{
"taskId": 55,
"taskTitle": "Implement Positional Arguments Support for CLI Commands",
"complexityScore": 5,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing positional arguments support appear well-structured. Consider if any additional subtasks are needed for backward compatibility testing, documentation updates, or user experience improvements.",
"reasoning": "This task involves modifying the command parsing logic to support positional arguments alongside the existing flag-based syntax. The complexity is moderate as it requires careful handling of different argument styles and edge cases. The 5 existing subtasks cover the main implementation areas from analysis to documentation."
},
{
"taskId": 57,
"taskTitle": "Enhance Task-Master CLI User Experience and Interface",
"complexityScore": 7,
"recommendedSubtasks": 6,
"expansionPrompt": "The current 6 subtasks for enhancing the CLI user experience appear comprehensive. Consider if any additional subtasks are needed for accessibility testing, internationalization, or performance optimization.",
"reasoning": "This task involves a significant overhaul of the CLI interface to improve user experience. The complexity is high due to the breadth of changes (logging, visual elements, interactive components, etc.) and the need for consistent design across all commands. The 6 existing subtasks cover the main implementation areas from log management to help systems."
},
{
"taskId": 60,
"taskTitle": "Implement Mentor System with Round-Table Discussion Feature",
"complexityScore": 8,
"recommendedSubtasks": 7,
"expansionPrompt": "The current 7 subtasks for implementing the mentor system appear well-structured. Consider if any additional subtasks are needed for mentor personality consistency, discussion quality evaluation, or performance optimization with multiple mentors.",
"reasoning": "This task involves creating a sophisticated mentor simulation system with round-table discussions. The complexity is high due to the need for personality simulation, complex LLM integration, and structured discussion management. The 7 existing subtasks cover the main implementation areas from architecture to testing."
},
{
"taskId": 62,
"taskTitle": "Add --simple Flag to Update Commands for Direct Text Input",
"complexityScore": 4,
"recommendedSubtasks": 8,
"expansionPrompt": "The current 8 subtasks for implementing the --simple flag appear comprehensive. Consider if any additional subtasks are needed for user experience testing or documentation updates.",
"reasoning": "This task involves adding a simple flag option to bypass AI processing for updates. The complexity is relatively low as it primarily involves modifying existing command handlers and adding a flag. The 8 existing subtasks are very detailed and cover all aspects of implementation from command parsing to testing."
},
{
"taskId": 63,
"taskTitle": "Add pnpm Support for the Taskmaster Package",
"complexityScore": 5,
"recommendedSubtasks": 8,
"expansionPrompt": "The current 8 subtasks for adding pnpm support appear comprehensive. Consider if any additional subtasks are needed for CI/CD integration, performance comparison, or documentation updates.",
"reasoning": "This task involves ensuring the package works correctly with pnpm as an alternative package manager. The complexity is moderate as it requires careful testing of installation processes and scripts across different environments. The 8 existing subtasks cover all major aspects from documentation to binary verification."
},
{
"taskId": 64,
"taskTitle": "Add Yarn Support for Taskmaster Installation",
"complexityScore": 5,
"recommendedSubtasks": 9,
"expansionPrompt": "The current 9 subtasks for adding Yarn support appear comprehensive. Consider if any additional subtasks are needed for performance testing, CI/CD integration, or compatibility with different Yarn versions.",
"reasoning": "This task involves ensuring the package works correctly with Yarn as an alternative package manager. The complexity is moderate as it requires careful testing of installation processes and scripts across different environments. The 9 existing subtasks are very detailed and cover all aspects from configuration to testing."
},
{
"taskId": 65,
"taskTitle": "Add Bun Support for Taskmaster Installation",
"complexityScore": 6,
"recommendedSubtasks": 6,
"expansionPrompt": "The current 6 subtasks for adding Bun support appear well-structured. Consider if any additional subtasks are needed for handling Bun-specific issues, performance testing, or documentation updates.",
"reasoning": "This task involves adding support for the newer Bun package manager. The complexity is slightly higher than the other package manager tasks due to Bun's differences from Node.js and potential compatibility issues. The 6 existing subtasks cover the main implementation areas from research to documentation."
},
{
"taskId": 67,
"taskTitle": "Add CLI JSON output and Cursor keybindings integration",
"complexityScore": 5,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing JSON output and Cursor keybindings appear well-structured. Consider if any additional subtasks are needed for testing across different operating systems, documentation updates, or user experience improvements.",
"reasoning": "This task involves two distinct features: adding JSON output to CLI commands and creating a keybindings installation command. The complexity is moderate as it requires careful handling of different output formats and OS-specific file paths. The 5 existing subtasks cover the main implementation areas for both features."
},
{
"taskId": 68,
"taskTitle": "Ability to create tasks without parsing PRD",
"complexityScore": 3,
"recommendedSubtasks": 2,
"expansionPrompt": "The current 2 subtasks for implementing task creation without PRD appear appropriate. Consider if any additional subtasks are needed for validation, error handling, or integration with existing task management workflows.",
"reasoning": "This task involves a relatively simple modification to allow task creation without requiring a PRD document. The complexity is low as it primarily involves creating a form interface and saving functionality. The 2 existing subtasks cover the main implementation areas of UI design and data saving."
},
{
"taskId": 72,
"taskTitle": "Implement PDF Generation for Project Progress and Dependency Overview",
"complexityScore": 7,
"recommendedSubtasks": 6,
"expansionPrompt": "The current 6 subtasks for implementing PDF generation appear comprehensive. Consider if any additional subtasks are needed for handling large projects, additional visualization options, or integration with existing reporting tools.",
"reasoning": "This task involves creating a feature to generate PDF reports of project progress and dependency visualization. The complexity is high due to the need for PDF generation, data collection, and visualization integration. The 6 existing subtasks cover the main implementation areas from library selection to export options."
},
{
"taskId": 75,
"taskTitle": "Integrate Google Search Grounding for Research Role",
"complexityScore": 5,
"recommendedSubtasks": 4,
"expansionPrompt": "The current 4 subtasks for integrating Google Search Grounding appear well-structured. Consider if any additional subtasks are needed for testing with different query types, error handling, or performance optimization.",
"reasoning": "This task involves updating the AI service layer to enable Google Search Grounding for research roles. The complexity is moderate as it requires careful integration with the existing AI service architecture and conditional logic. The 4 existing subtasks cover the main implementation areas from service layer modification to testing."
},
{
"taskId": 76,
"taskTitle": "Develop E2E Test Framework for Taskmaster MCP Server (FastMCP over stdio)",
"complexityScore": 8,
"recommendedSubtasks": 7,
"expansionPrompt": "The current 7 subtasks for developing the E2E test framework appear comprehensive. Consider if any additional subtasks are needed for test result reporting, CI/CD integration, or performance benchmarking.",
"reasoning": "This task involves creating a sophisticated end-to-end testing framework for the MCP server. The complexity is high due to the need for subprocess management, protocol handling, and robust test case definition. The 7 existing subtasks cover the main implementation areas from architecture to documentation."
},
{
"taskId": 77,
"taskTitle": "Implement AI Usage Telemetry for Taskmaster (with external analytics endpoint)",
"complexityScore": 7,
"recommendedSubtasks": 18,
"expansionPrompt": "The current 18 subtasks for implementing AI usage telemetry appear very comprehensive. Consider if any additional subtasks are needed for security hardening, privacy compliance, or user feedback collection.",
"reasoning": "This task involves creating a telemetry system to track AI usage metrics. The complexity is high due to the need for secure data transmission, comprehensive data collection, and integration across multiple commands. The 18 existing subtasks are extremely detailed and cover all aspects of implementation from core utility to provider-specific updates."
},
{
"taskId": 80,
"taskTitle": "Implement Unique User ID Generation and Storage During Installation",
"complexityScore": 4,
"recommendedSubtasks": 5,
"expansionPrompt": "The current 5 subtasks for implementing unique user ID generation appear well-structured. Consider if any additional subtasks are needed for privacy compliance, security auditing, or integration with the telemetry system.",
"reasoning": "This task involves generating and storing a unique user identifier during installation. The complexity is relatively low as it primarily involves UUID generation and configuration file management. The 5 existing subtasks cover the main implementation areas from script structure to documentation."
},
{
"taskId": 81,
"taskTitle": "Task #81: Implement Comprehensive Local Telemetry System with Future Server Integration Capability",
"complexityScore": 8,
"recommendedSubtasks": 6,
"expansionPrompt": "The current 6 subtasks for implementing the comprehensive local telemetry system appear well-structured. Consider if any additional subtasks are needed for data migration, storage optimization, or visualization tools.",
"reasoning": "This task involves expanding the telemetry system to capture additional metrics and implement local storage with future server integration capability. The complexity is high due to the breadth of data collection, storage requirements, and privacy considerations. The 6 existing subtasks cover the main implementation areas from data collection to user-facing benefits."
},
{
"taskId": 82,
"taskTitle": "Update supported-models.json with token limit fields",
"complexityScore": 3,
"recommendedSubtasks": 1,
"expansionPrompt": "This task appears straightforward enough to be implemented without further subtasks. Focus on researching accurate token limit values for each model and ensuring backward compatibility.",
"reasoning": "This task involves a simple update to the supported-models.json file to include new token limit fields. The complexity is low as it primarily involves research and data entry. No subtasks are necessary as the task is well-defined and focused."
},
{
"taskId": 83,
"taskTitle": "Update config-manager.js defaults and getters",
"complexityScore": 4,
"recommendedSubtasks": 1,
"expansionPrompt": "This task appears straightforward enough to be implemented without further subtasks. Focus on updating the DEFAULTS object and related getter functions while maintaining backward compatibility.",
"reasoning": "This task involves updating the config-manager.js module to replace maxTokens with more specific token limit fields. The complexity is relatively low as it primarily involves modifying existing code rather than creating new functionality. No subtasks are necessary as the task is well-defined and focused."
},
{
"taskId": 84,
"taskTitle": "Implement token counting utility",
"complexityScore": 5,
"recommendedSubtasks": 1,
"expansionPrompt": "This task appears well-defined enough to be implemented without further subtasks. Focus on implementing accurate token counting for different models and proper fallback mechanisms.",
"reasoning": "This task involves creating a utility function to count tokens for different AI models. The complexity is moderate as it requires integration with the tiktoken library and handling different tokenization schemes. No subtasks are necessary as the task is well-defined and focused."
},
{
"taskId": 69,
"taskTitle": "Enhance Analyze Complexity for Specific Task IDs",
"complexityScore": 7,
"recommendedSubtasks": 6,
"expansionPrompt": "Break down the task 'Enhance Analyze Complexity for Specific Task IDs' into 6 subtasks focusing on: 1) Core logic modification to accept ID parameters, 2) Report merging functionality, 3) CLI interface updates, 4) MCP tool integration, 5) Documentation updates, and 6) Comprehensive testing across all components.",
"reasoning": "This task involves modifying existing functionality across multiple components (core logic, CLI, MCP) with complex logic for filtering tasks and merging reports. The implementation requires careful handling of different parameter combinations and edge cases. The task has interdependent components that need to work together seamlessly, and the report merging functionality adds significant complexity."
},
{
"taskId": 70,
"taskTitle": "Implement 'diagram' command for Mermaid diagram generation",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "Break down the 'diagram' command implementation into 5 subtasks: 1) Command interface and parameter handling, 2) Task data extraction and transformation to Mermaid syntax, 3) Diagram rendering with status color coding, 4) Output formatting and file export functionality, and 5) Error handling and edge case management.",
"reasoning": "This task requires implementing a new feature rather than modifying existing code, which reduces complexity from integration challenges. However, it involves working with visualization logic, dependency mapping, and multiple output formats. The color coding based on status and handling of dependency relationships adds moderate complexity. The task is well-defined but requires careful attention to diagram formatting and error handling."
},
{
"taskId": 85,
"taskTitle": "Update ai-services-unified.js for dynamic token limits",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Break down the update of ai-services-unified.js for dynamic token limits into subtasks such as: (1) Import and integrate the token counting utility, (2) Refactor _unifiedServiceRunner to calculate and enforce dynamic token limits, (3) Update error handling for token limit violations, (4) Add and verify logging for token usage, (5) Write and execute tests for various prompt and model scenarios.",
"reasoning": "This task involves significant code changes to a core function, integration of a new utility, dynamic logic for multiple models, and robust error handling. It also requires comprehensive testing for edge cases and integration, making it moderately complex and best managed by splitting into focused subtasks."
},
{
"taskId": 87,
"taskTitle": "Implement validation and error handling",
"complexityScore": 5,
"recommendedSubtasks": 4,
"expansionPrompt": "Decompose this task into: (1) Add validation logic for model and config loading, (2) Implement error handling and fallback mechanisms, (3) Enhance logging and reporting for token usage, (4) Develop helper functions for configuration suggestions and improvements.",
"reasoning": "This task is primarily about adding validation, error handling, and logging. While important for robustness, the logic is straightforward and can be modularized into a few clear subtasks."
},
{
"taskId": 89,
"taskTitle": "Introduce Prioritize Command with Enhanced Priority Levels",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "Expand this task into: (1) Implement the prioritize command with all required flags and shorthands, (2) Update CLI output and help documentation for new priority levels, (3) Ensure backward compatibility with existing commands, (4) Add error handling for invalid inputs, (5) Write and run tests for all command scenarios.",
"reasoning": "This CLI feature requires command parsing, updating internal logic for new priority levels, documentation, and robust error handling. The complexity is moderate due to the need for backward compatibility and comprehensive testing."
},
{
"taskId": 90,
"taskTitle": "Implement Subtask Progress Analyzer and Reporting System",
"complexityScore": 8,
"recommendedSubtasks": 6,
"expansionPrompt": "Break down the analyzer implementation into: (1) Design and implement progress tracking logic, (2) Develop status validation and issue detection, (3) Build the reporting system with multiple output formats, (4) Integrate analyzer with the existing task management system, (5) Optimize for performance and scalability, (6) Write unit, integration, and performance tests.",
"reasoning": "This is a complex, multi-faceted feature involving data analysis, reporting, integration, and performance optimization. It touches many parts of the system and requires careful design, making it one of the most complex tasks in the list."
},
{
"taskId": 91,
"taskTitle": "Implement Move Command for Tasks and Subtasks",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Expand this task into: (1) Implement move logic for tasks and subtasks, (2) Handle edge cases (invalid ids, non-existent parents, circular dependencies), (3) Update CLI to support move command with flags, (4) Ensure data integrity and update relationships, (5) Write and execute tests for various move scenarios.",
"reasoning": "Moving tasks and subtasks requires careful handling of hierarchical data, edge cases, and data integrity. The command must be robust and user-friendly, necessitating multiple focused subtasks for safe implementation."
},
{
"taskId": 92,
"taskTitle": "Add Global Joke Flag to All CLI Commands",
"complexityScore": 8,
"recommendedSubtasks": 7,
"expansionPrompt": "Break down the implementation of the global --joke flag into the following subtasks: (1) Update CLI foundation to support global flags, (2) Develop the joke-service module with joke management and category support, (3) Integrate joke output into existing output utilities, (4) Update all CLI commands for joke flag compatibility, (5) Add configuration options for joke categories and custom jokes, (6) Implement comprehensive testing (flag recognition, output, content, integration, performance, regression), (7) Update documentation and usage examples.",
"reasoning": "This task requires changes across the CLI foundation, output utilities, all command modules, and configuration management. It introduces a new service module, global flag handling, and output logic that must not interfere with existing features (including JSON output). The need for robust testing and backward compatibility further increases complexity. The scope spans multiple code areas and requires careful integration, justifying a high complexity score and a detailed subtask breakdown to manage risk and ensure maintainability.[2][3][5]"
},
{
"taskId": 94,
"taskTitle": "Implement Standalone 'research' CLI Command for AI-Powered Queries",
"complexityScore": 7,
"recommendedSubtasks": 6,
"expansionPrompt": "Break down the implementation of the 'research' CLI command into logical subtasks covering command registration, parameter handling, context gathering, AI service integration, output formatting, and documentation.",
"reasoning": "This task has moderate to high complexity (7/10) due to multiple interconnected components: CLI argument parsing, integration with AI services, context gathering from various sources, and output formatting with different modes. The cyclomatic complexity would be significant with multiple decision paths for handling different flags and options. The task requires understanding existing patterns and extending the codebase in a consistent manner, suggesting the need for careful decomposition into manageable subtasks."
},
{
"taskId": 86,
"taskTitle": "Implement GitHub Issue Export Feature",
"complexityScore": 9,
"recommendedSubtasks": 10,
"expansionPrompt": "Break down the implementation of the GitHub Issue Export Feature into detailed subtasks covering: command structure and CLI integration, GitHub API client development, authentication and error handling, task-to-issue mapping logic, content formatting and markdown conversion, bidirectional linking and metadata management, extensible architecture and adapter interfaces, configuration and settings management, documentation, and comprehensive testing (unit, integration, edge cases, performance).",
"reasoning": "This task involves designing and implementing a robust, extensible export system with deep integration into GitHub, including bidirectional workflows, complex data mapping, error handling, and support for future platforms. The requirements span CLI design, API integration, content transformation, metadata management, extensibility, configuration, and extensive testing. The breadth and depth of these requirements, along with the need for maintainability and future extensibility, place this task at a high complexity level. Breaking it into at least 10 subtasks will ensure each major component and concern is addressed systematically, reducing risk and improving quality."
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"currentTag": "master", "currentTag": "master",
"lastSwitched": "2025-07-22T13:32:03.558Z", "lastSwitched": "2025-08-01T14:09:25.838Z",
"branchTagMapping": { "branchTagMapping": {
"v017-adds": "v017-adds", "v017-adds": "v017-adds",
"next": "next" "next": "next"

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,229 @@
# task-master-ai # task-master-ai
## 0.23.0
### Minor Changes
- [#1064](https://github.com/eyaltoledano/claude-task-master/pull/1064) [`53903f1`](https://github.com/eyaltoledano/claude-task-master/commit/53903f1e8eee23ac512eb13a6d81d8cbcfe658cb) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add new `scope-up` and `scope-down` commands for dynamic task complexity adjustment
This release introduces two powerful new commands that allow you to dynamically adjust the complexity of your tasks and subtasks without recreating them from scratch.
**New CLI Commands:**
- `task-master scope-up` - Increase task complexity (add more detail, requirements, or implementation steps)
- `task-master scope-down` - Decrease task complexity (simplify, remove unnecessary details, or streamline)
**Key Features:**
- **Multiple tasks**: Support comma-separated IDs to adjust multiple tasks at once (`--id=5,7,12`)
- **Strength levels**: Choose adjustment intensity with `--strength=light|regular|heavy` (defaults to regular)
- **Custom prompts**: Use `--prompt` flag to specify exactly how you want tasks adjusted
- **MCP integration**: Available as `scope_up_task` and `scope_down_task` tools in Cursor and other MCP environments
- **Smart context**: AI considers your project context and task dependencies when making adjustments
**Usage Examples:**
```bash
# Make a task more detailed
task-master scope-up --id=5
# Simplify multiple tasks with light touch
task-master scope-down --id=10,11,12 --strength=light
# Custom adjustment with specific instructions
task-master scope-up --id=7 --prompt="Add more error handling and edge cases"
```
**Why use this?**
- **Iterative refinement**: Adjust task complexity as your understanding evolves
- **Project phase adaptation**: Scale tasks up for implementation, down for planning
- **Team coordination**: Adjust complexity based on team member experience levels
- **Milestone alignment**: Fine-tune tasks to match project phase requirements
Perfect for agile workflows where task requirements change as you learn more about the problem space.
### Patch Changes
- [#1063](https://github.com/eyaltoledano/claude-task-master/pull/1063) [`2ae6e7e`](https://github.com/eyaltoledano/claude-task-master/commit/2ae6e7e6be3605c3c4d353f34666e54750dba973) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix for tasks not found when using string IDs
- [#1049](https://github.com/eyaltoledano/claude-task-master/pull/1049) [`45a14c3`](https://github.com/eyaltoledano/claude-task-master/commit/45a14c323d21071c15106335e89ad1f4a20976ab) Thanks [@ben-vargas](https://github.com/ben-vargas)! - Fix tag-specific complexity report detection in expand command
The expand command now correctly finds and uses tag-specific complexity reports (e.g., `task-complexity-report_feature-xyz.json`) when operating in a tag context. Previously, it would always look for the generic `task-complexity-report.json` file due to a default value in the CLI option definition.
## 0.23.0-rc.2
### Minor Changes
- [#1064](https://github.com/eyaltoledano/claude-task-master/pull/1064) [`53903f1`](https://github.com/eyaltoledano/claude-task-master/commit/53903f1e8eee23ac512eb13a6d81d8cbcfe658cb) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add new `scope-up` and `scope-down` commands for dynamic task complexity adjustment
This release introduces two powerful new commands that allow you to dynamically adjust the complexity of your tasks and subtasks without recreating them from scratch.
**New CLI Commands:**
- `task-master scope-up` - Increase task complexity (add more detail, requirements, or implementation steps)
- `task-master scope-down` - Decrease task complexity (simplify, remove unnecessary details, or streamline)
**Key Features:**
- **Multiple tasks**: Support comma-separated IDs to adjust multiple tasks at once (`--id=5,7,12`)
- **Strength levels**: Choose adjustment intensity with `--strength=light|regular|heavy` (defaults to regular)
- **Custom prompts**: Use `--prompt` flag to specify exactly how you want tasks adjusted
- **MCP integration**: Available as `scope_up_task` and `scope_down_task` tools in Cursor and other MCP environments
- **Smart context**: AI considers your project context and task dependencies when making adjustments
**Usage Examples:**
```bash
# Make a task more detailed
task-master scope-up --id=5
# Simplify multiple tasks with light touch
task-master scope-down --id=10,11,12 --strength=light
# Custom adjustment with specific instructions
task-master scope-up --id=7 --prompt="Add more error handling and edge cases"
```
**Why use this?**
- **Iterative refinement**: Adjust task complexity as your understanding evolves
- **Project phase adaptation**: Scale tasks up for implementation, down for planning
- **Team coordination**: Adjust complexity based on team member experience levels
- **Milestone alignment**: Fine-tune tasks to match project phase requirements
Perfect for agile workflows where task requirements change as you learn more about the problem space.
## 0.22.1-rc.1
### Patch Changes
- [#1069](https://github.com/eyaltoledano/claude-task-master/pull/1069) [`72ca68e`](https://github.com/eyaltoledano/claude-task-master/commit/72ca68edeb870ff7a3b0d2d632e09dae921dc16a) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add new `scope-up` and `scope-down` commands for dynamic task complexity adjustment
This release introduces two powerful new commands that allow you to dynamically adjust the complexity of your tasks and subtasks without recreating them from scratch.
**New CLI Commands:**
- `task-master scope-up` - Increase task complexity (add more detail, requirements, or implementation steps)
- `task-master scope-down` - Decrease task complexity (simplify, remove unnecessary details, or streamline)
**Key Features:**
- **Multiple tasks**: Support comma-separated IDs to adjust multiple tasks at once (`--id=5,7,12`)
- **Strength levels**: Choose adjustment intensity with `--strength=light|regular|heavy` (defaults to regular)
- **Custom prompts**: Use `--prompt` flag to specify exactly how you want tasks adjusted
- **MCP integration**: Available as `scope_up_task` and `scope_down_task` tools in Cursor and other MCP environments
- **Smart context**: AI considers your project context and task dependencies when making adjustments
**Usage Examples:**
```bash
# Make a task more detailed
task-master scope-up --id=5
# Simplify multiple tasks with light touch
task-master scope-down --id=10,11,12 --strength=light
# Custom adjustment with specific instructions
task-master scope-up --id=7 --prompt="Add more error handling and edge cases"
```
**Why use this?**
- **Iterative refinement**: Adjust task complexity as your understanding evolves
- **Project phase adaptation**: Scale tasks up for implementation, down for planning
- **Team coordination**: Adjust complexity based on team member experience levels
- **Milestone alignment**: Fine-tune tasks to match project phase requirements
Perfect for agile workflows where task requirements change as you learn more about the problem space.
## 0.22.1-rc.0
### Patch Changes
- [#1063](https://github.com/eyaltoledano/claude-task-master/pull/1063) [`2ae6e7e`](https://github.com/eyaltoledano/claude-task-master/commit/2ae6e7e6be3605c3c4d353f34666e54750dba973) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix for tasks not found when using string IDs
- [#1049](https://github.com/eyaltoledano/claude-task-master/pull/1049) [`45a14c3`](https://github.com/eyaltoledano/claude-task-master/commit/45a14c323d21071c15106335e89ad1f4a20976ab) Thanks [@ben-vargas](https://github.com/ben-vargas)! - Fix tag-specific complexity report detection in expand command
The expand command now correctly finds and uses tag-specific complexity reports (e.g., `task-complexity-report_feature-xyz.json`) when operating in a tag context. Previously, it would always look for the generic `task-complexity-report.json` file due to a default value in the CLI option definition.
## 0.22.0
### Minor Changes
- [#1043](https://github.com/eyaltoledano/claude-task-master/pull/1043) [`dc44ed9`](https://github.com/eyaltoledano/claude-task-master/commit/dc44ed9de8a57aca5d39d3a87565568bd0a82068) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Prompt to generate a complexity report when it is missing
- [#1032](https://github.com/eyaltoledano/claude-task-master/pull/1032) [`4423119`](https://github.com/eyaltoledano/claude-task-master/commit/4423119a5ec53958c9dffa8bf564da8be7a2827d) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add comprehensive Kiro IDE integration with autonomous task management hooks
- **Kiro Profile**: Added full support for Kiro IDE with automatic installation of 7 Taskmaster agent hooks
- **Hook-Driven Workflow**: Introduced natural language automation hooks that eliminate manual task status updates
- **Automatic Hook Installation**: Hooks are now automatically copied to `.kiro/hooks/` when running `task-master rules add kiro`
- **Language-Agnostic Support**: All hooks support multiple programming languages (JS, Python, Go, Rust, Java, etc.)
- **Frontmatter Transformation**: Kiro rules use simplified `inclusion: always` format instead of Cursor's complex frontmatter
- **Special Rule**: Added `taskmaster_hooks_workflow.md` that guides AI assistants to prefer hook-driven completion
Key hooks included:
- Task Dependency Auto-Progression: Automatically starts tasks when dependencies complete
- Code Change Task Tracker: Updates task progress as you save files
- Test Success Task Completer: Marks tasks done when tests pass
- Daily Standup Assistant: Provides personalized task status summaries
- PR Readiness Checker: Validates task completion before creating pull requests
- Complexity Analyzer: Auto-expands complex tasks into manageable subtasks
- Git Commit Task Linker: Links commits to tasks for better traceability
This creates a truly autonomous development workflow where task management happens naturally as you code!
### Patch Changes
- [#1033](https://github.com/eyaltoledano/claude-task-master/pull/1033) [`7b90568`](https://github.com/eyaltoledano/claude-task-master/commit/7b9056832653464f934c91c22997077065d738c4) Thanks [@ben-vargas](https://github.com/ben-vargas)! - Fix compatibility with @google/gemini-cli-core v0.1.12+ by updating ai-sdk-provider-gemini-cli to v0.1.1.
- [#1038](https://github.com/eyaltoledano/claude-task-master/pull/1038) [`77cc5e4`](https://github.com/eyaltoledano/claude-task-master/commit/77cc5e4537397642f2664f61940a101433ee6fb4) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix 'expand --all' and 'show' commands to correctly handle tag contexts for complexity reports and task display.
- [#1025](https://github.com/eyaltoledano/claude-task-master/pull/1025) [`8781794`](https://github.com/eyaltoledano/claude-task-master/commit/8781794c56d454697fc92c88a3925982d6b81205) Thanks [@joedanz](https://github.com/joedanz)! - Clean up remaining automatic task file generation calls
- [#1035](https://github.com/eyaltoledano/claude-task-master/pull/1035) [`fb7d588`](https://github.com/eyaltoledano/claude-task-master/commit/fb7d588137e8c53b0d0f54bd1dd8d387648583ee) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix max_tokens limits for OpenRouter and Groq models
- Add special handling in config-manager.js for custom OpenRouter models to use a conservative default of 32,768 max_tokens
- Update qwen/qwen-turbo model max_tokens from 1,000,000 to 32,768 to match OpenRouter's actual limits
- Fix moonshotai/kimi-k2-instruct max_tokens to 16,384 to match Groq's actual limit (fixes #1028)
- This prevents "maximum context length exceeded" errors when using OpenRouter models not in our supported models list
- [#1027](https://github.com/eyaltoledano/claude-task-master/pull/1027) [`6ae66b2`](https://github.com/eyaltoledano/claude-task-master/commit/6ae66b2afbfe911340fa25e0236c3db83deaa7eb) Thanks [@andreswebs](https://github.com/andreswebs)! - Fix VSCode profile generation to use correct rule file names (using `.instructions.md` extension instead of `.md`) and front-matter properties (removing the unsupported `alwaysApply` property from instructions files' front-matter).
## 0.22.0-rc.1
### Minor Changes
- [#1043](https://github.com/eyaltoledano/claude-task-master/pull/1043) [`dc44ed9`](https://github.com/eyaltoledano/claude-task-master/commit/dc44ed9de8a57aca5d39d3a87565568bd0a82068) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Prompt to generate a complexity report when it is missing
## 0.22.0-rc.0
### Minor Changes
- [#1032](https://github.com/eyaltoledano/claude-task-master/pull/1032) [`4423119`](https://github.com/eyaltoledano/claude-task-master/commit/4423119a5ec53958c9dffa8bf564da8be7a2827d) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add comprehensive Kiro IDE integration with autonomous task management hooks
- **Kiro Profile**: Added full support for Kiro IDE with automatic installation of 7 Taskmaster agent hooks
- **Hook-Driven Workflow**: Introduced natural language automation hooks that eliminate manual task status updates
- **Automatic Hook Installation**: Hooks are now automatically copied to `.kiro/hooks/` when running `task-master rules add kiro`
- **Language-Agnostic Support**: All hooks support multiple programming languages (JS, Python, Go, Rust, Java, etc.)
- **Frontmatter Transformation**: Kiro rules use simplified `inclusion: always` format instead of Cursor's complex frontmatter
- **Special Rule**: Added `taskmaster_hooks_workflow.md` that guides AI assistants to prefer hook-driven completion
Key hooks included:
- Task Dependency Auto-Progression: Automatically starts tasks when dependencies complete
- Code Change Task Tracker: Updates task progress as you save files
- Test Success Task Completer: Marks tasks done when tests pass
- Daily Standup Assistant: Provides personalized task status summaries
- PR Readiness Checker: Validates task completion before creating pull requests
- Complexity Analyzer: Auto-expands complex tasks into manageable subtasks
- Git Commit Task Linker: Links commits to tasks for better traceability
This creates a truly autonomous development workflow where task management happens naturally as you code!
### Patch Changes
- [#1033](https://github.com/eyaltoledano/claude-task-master/pull/1033) [`7b90568`](https://github.com/eyaltoledano/claude-task-master/commit/7b9056832653464f934c91c22997077065d738c4) Thanks [@ben-vargas](https://github.com/ben-vargas)! - Fix compatibility with @google/gemini-cli-core v0.1.12+ by updating ai-sdk-provider-gemini-cli to v0.1.1.
- [#1038](https://github.com/eyaltoledano/claude-task-master/pull/1038) [`77cc5e4`](https://github.com/eyaltoledano/claude-task-master/commit/77cc5e4537397642f2664f61940a101433ee6fb4) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix 'expand --all' and 'show' commands to correctly handle tag contexts for complexity reports and task display.
- [#1025](https://github.com/eyaltoledano/claude-task-master/pull/1025) [`8781794`](https://github.com/eyaltoledano/claude-task-master/commit/8781794c56d454697fc92c88a3925982d6b81205) Thanks [@joedanz](https://github.com/joedanz)! - Clean up remaining automatic task file generation calls
- [#1035](https://github.com/eyaltoledano/claude-task-master/pull/1035) [`fb7d588`](https://github.com/eyaltoledano/claude-task-master/commit/fb7d588137e8c53b0d0f54bd1dd8d387648583ee) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix max_tokens limits for OpenRouter and Groq models
- Add special handling in config-manager.js for custom OpenRouter models to use a conservative default of 32,768 max_tokens
- Update qwen/qwen-turbo model max_tokens from 1,000,000 to 32,768 to match OpenRouter's actual limits
- Fix moonshotai/kimi-k2-instruct max_tokens to 16,384 to match Groq's actual limit (fixes #1028)
- This prevents "maximum context length exceeded" errors when using OpenRouter models not in our supported models list
- [#1027](https://github.com/eyaltoledano/claude-task-master/pull/1027) [`6ae66b2`](https://github.com/eyaltoledano/claude-task-master/commit/6ae66b2afbfe911340fa25e0236c3db83deaa7eb) Thanks [@andreswebs](https://github.com/andreswebs)! - Fix VSCode profile generation to use correct rule file names (using `.instructions.md` extension instead of `.md`) and front-matter properties (removing the unsupported `alwaysApply` property from instructions files' front-matter).
## 0.21.0 ## 0.21.0
### Minor Changes ### Minor Changes

View File

@@ -0,0 +1,25 @@
# Ignore everything by default
*
# Only include specific essential files
!package.json
!README.md
!CHANGELOG.md
!LICENSE
!icon.png
!assets/**
# Include only the built files we need (not source maps)
!dist/extension.js
!dist/index.js
!dist/index.css
# Exclude development documentation
docs/extension-CI-setup.md
docs/extension-DEV-guide.md
# Exclude
assets/.DS_Store
assets/banner.png

130
apps/extension/CHANGELOG.md Normal file
View File

@@ -0,0 +1,130 @@
# Change Log
## 0.23.0
### Minor Changes
- [#1064](https://github.com/eyaltoledano/claude-task-master/pull/1064) [`b82d858`](https://github.com/eyaltoledano/claude-task-master/commit/b82d858f81a1e702ad59d84d5ae8a2ca84359a83) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 🎉 **Introducing TaskMaster Extension!**
We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow.
## ✨ Key Features
### 📋 Visual Task Management
- **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code
- **Drag & Drop**: Easily change task status by dragging cards between columns
- **Real-time Updates**: See changes instantly as you work through your project
### 🏷️ Multi-Context Support
- **Tag Switching**: Seamlessly switch between different project contexts/tags
- **Isolated Workflows**: Keep different features or experiments organized separately
### 🤖 AI-Powered Task Updates
- **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks
- **Context-Aware**: Leverages your existing TaskMaster configuration and models
### 📊 Rich Task Information
- **Complexity Scores**: See task complexity ratings at a glance
- **Subtask Visualization**: Expand tasks to view and manage subtasks
- **Dependency Graphs**: Understand task relationships and dependencies visually
### ⚙️ Configuration Management
- **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings
- **Easy Access**: No more manual JSON editing for common configuration tasks
### 🚀 Quick Actions
- **Status Updates**: Change task status with a single click
- **Task Details**: Access full task information without leaving VS Code
- **Integrated Commands**: All TaskMaster commands available through the command palette
## 🎯 What's Next?
This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow.
Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀
## 0.23.0-rc.1
### Minor Changes
- [#1064](https://github.com/eyaltoledano/claude-task-master/pull/1064) [`b82d858`](https://github.com/eyaltoledano/claude-task-master/commit/b82d858f81a1e702ad59d84d5ae8a2ca84359a83) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 🎉 **Introducing TaskMaster Extension!**
We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow.
## ✨ Key Features
### 📋 Visual Task Management
- **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code
- **Drag & Drop**: Easily change task status by dragging cards between columns
- **Real-time Updates**: See changes instantly as you work through your project
### 🏷️ Multi-Context Support
- **Tag Switching**: Seamlessly switch between different project contexts/tags
- **Isolated Workflows**: Keep different features or experiments organized separately
### 🤖 AI-Powered Task Updates
- **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks
- **Context-Aware**: Leverages your existing TaskMaster configuration and models
### 📊 Rich Task Information
- **Complexity Scores**: See task complexity ratings at a glance
- **Subtask Visualization**: Expand tasks to view and manage subtasks
- **Dependency Graphs**: Understand task relationships and dependencies visually
### ⚙️ Configuration Management
- **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings
- **Easy Access**: No more manual JSON editing for common configuration tasks
### 🚀 Quick Actions
- **Status Updates**: Change task status with a single click
- **Task Details**: Access full task information without leaving VS Code
- **Integrated Commands**: All TaskMaster commands available through the command palette
## 🎯 What's Next?
This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow.
Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀
## 0.23.0-rc.0
### Minor Changes
- [#997](https://github.com/eyaltoledano/claude-task-master/pull/997) [`64302dc`](https://github.com/eyaltoledano/claude-task-master/commit/64302dc1918f673fcdac05b29411bf76ffe93505) Thanks [@DavidMaliglowka](https://github.com/DavidMaliglowka)! - 🎉 **Introducing TaskMaster Extension!**
We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow.
## ✨ Key Features
### 📋 Visual Task Management
- **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code
- **Drag & Drop**: Easily change task status by dragging cards between columns
- **Real-time Updates**: See changes instantly as you work through your project
### 🏷️ Multi-Context Support
- **Tag Switching**: Seamlessly switch between different project contexts/tags
- **Isolated Workflows**: Keep different features or experiments organized separately
### 🤖 AI-Powered Task Updates
- **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks
- **Context-Aware**: Leverages your existing TaskMaster configuration and models
### 📊 Rich Task Information
- **Complexity Scores**: See task complexity ratings at a glance
- **Subtask Visualization**: Expand tasks to view and manage subtasks
- **Dependency Graphs**: Understand task relationships and dependencies visually
### ⚙️ Configuration Management
- **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings
- **Easy Access**: No more manual JSON editing for common configuration tasks
### 🚀 Quick Actions
- **Status Updates**: Change task status with a single click
- **Task Details**: Access full task information without leaving VS Code
- **Integrated Commands**: All TaskMaster commands available through the command palette
## 🎯 What's Next?
This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow.
Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀

21
apps/extension/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 David Maliglowka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

204
apps/extension/README.md Normal file
View File

@@ -0,0 +1,204 @@
# Official Taskmaster AI Extension
Transform your AI-driven development workflow with a beautiful, interactive Kanban board directly in VS Code. Seamlessly manage tasks from [Taskmaster AI](https://github.com/eyaltoledano/claude-task-master) projects with real-time synchronization and intelligent task management.
![Taskmaster AI Extension](https://img.shields.io/badge/VS%20Code-Extension-blue)
![License](https://img.shields.io/badge/License-MIT-green)
![Version](https://img.shields.io/visual-studio-marketplace/v/Hamster.task-master-hamster)
![Installs](https://img.shields.io/visual-studio-marketplace/i/Hamster.task-master-hamster)
## 🎯 What is Taskmaster AI?
Taskmaster AI is an intelligent task management system designed for AI-assisted development. It helps you break down complex projects into manageable tasks, track progress, and leverage AI to enhance your development workflow.
## ✨ Key Features
### 📊 **Interactive Kanban Board**
- **Drag & Drop Interface** - Effortlessly move tasks between status columns
- **Real-time Sync** - Changes instantly reflect in your Taskmaster project files
- **Multiple Views** - Board view and detailed task sidebar
- **Smart Columns** - Pending, In Progress, Review, Done, Deferred, and Cancelled
![Kanban Board](assets/screenshots/kanban-board.png)
### 🤖 **AI-Powered Features**
- **Task Content Generation** - Regenerate task descriptions using AI
- **Smart Task Updates** - Append findings and progress notes automatically
- **MCP Integration** - Seamless connection to Taskmaster AI via Model Context Protocol
- **Intelligent Caching** - Smart performance optimization with background refresh
![Task Details](assets/screenshots/task-details.png)
### 🚀 **Performance & Usability**
- **Offline Support** - Continue working even when disconnected
- **Auto-refresh** - Automatic polling for task changes with smart frequency
- **VS Code Native** - Perfectly integrated with VS Code themes and UI
- **Modern Interface** - Built with ShadCN UI components and Tailwind CSS
## 🛠️ Installation
### Prerequisites
1. **VS Code** 1.90.0 or higher
2. **Node.js** 18.0 or higher (for Taskmaster MCP server)
### Install the Extension
1. **From VS Code Marketplace:**
- Click the **Install** button above
- The extension will be automatically added to your VS Code instance
## 🚀 Quick Start
### 1. **Initialize Taskmaster Project**
If you don't have a Taskmaster project yet:
```bash
cd your-project
npm i -g task-master-ai
task-master init
```
### 2. **Open Kanban Board**
- **Command Palette** (Ctrl+Shift+P): `Taskmaster Kanban: Show Board`
- **Or** the extension automatically activates when you have a `.taskmaster` folder in your workspace
### 3. **MCP Server Setup**
The extension automatically handles the Taskmaster MCP server connection:
- **No manual installation required** - The extension spawns the MCP server automatically
- **Uses npx by default** - Automatically downloads Taskmaster AI when needed
- **Configurable** - You can customize the MCP server command in settings if needed
### 4. **Start Managing Tasks**
- **Drag tasks** between columns to change status
- **Click tasks** to view detailed information
- **Use AI features** to enhance task content
- **Add subtasks** with the + button on parent tasks
## 📋 Usage Guide
### Task Management
| Action | How to Do It |
|--------|--------------|
| **View Kanban Board** | `Ctrl/Cmd + Shift + P` → "Taskmaster: Show Board" |
| **Change Task Status** | Drag task card to different column |
| **View Task Details** | Click on any task card |
| **Edit Task Content** | Click task → Use edit buttons in details panel |
| **Add Subtasks** | Click the + button on parent task cards |
| **Use AI Features** | Open task details → Click AI action buttons |
### Understanding Task Statuses
- 📋 **Pending** - Tasks ready to be started
- 🚀 **In Progress** - Currently being worked on
- 👀 **Review** - Awaiting review or feedback
- ✅ **Done** - Completed tasks
- ⏸️ **Deferred** - Postponed for later
### **AI-Powered Task Management**
The extension integrates seamlessly with Taskmaster AI via MCP to provide:
- **Smart Task Generation** - AI creates detailed implementation plans
- **Progress Tracking** - Append timestamped notes and findings
- **Content Enhancement** - Regenerate task descriptions for clarity
- **Research Integration** - Get up-to-date information for your tasks
## ⚙️ Configuration
Access settings via **File → Preferences → Settings** and search for "Taskmaster":
### **MCP Connection Settings**
- **MCP Server Command** - Path to task-master-ai executable (default: `npx`)
- **MCP Server Args** - Arguments for the server command (default: `-y`, `--package=task-master-ai`, `task-master-ai`)
- **Connection Timeout** - Server response timeout (default: 30s)
- **Auto Refresh** - Enable automatic task updates (default: enabled)
### **UI Preferences**
- **Theme** - Auto, Light, or Dark mode
- **Show Completed Tasks** - Display done tasks in board (default: enabled)
- **Task Display Limit** - Maximum tasks to show (default: 100)
### **Performance Options**
- **Cache Duration** - How long to cache task data (default: 5s)
- **Concurrent Requests** - Max simultaneous API calls (default: 5)
## 🔧 Troubleshooting
### **Extension Not Loading**
1. Ensure Node.js 18+ is installed
2. Check workspace contains `.taskmaster` folder
3. Restart VS Code
4. Check Output panel (View → Output → Taskmaster Kanban)
### **MCP Connection Issues**
1. **Command not found**: Ensure Node.js and npx are in your PATH
2. **Timeout errors**: Increase timeout in settings
3. **Permission errors**: Check Node.js permissions
4. **Network issues**: Verify internet connection for npx downloads
### **Tasks Not Updating**
1. Check MCP connection status in status bar
2. Verify `.taskmaster/tasks/tasks.json` exists
3. Try manual refresh: `Taskmaster Kanban: Check Connection`
4. Review error logs in Output panel
### **Performance Issues**
1. Reduce task display limit in settings
2. Increase cache duration
3. Disable auto-refresh if needed
4. Close other VS Code extensions temporarily
## 🆘 Support & Resources
### **Getting Help**
- 📖 **Documentation**: [Taskmaster AI Docs](https://github.com/eyaltoledano/claude-task-master)
- 🐛 **Report Issues**: [GitHub Issues](https://github.com/eyaltoledano/claude-task-master/issues)
- 💬 **Discussions**: [GitHub Discussions](https://github.com/eyaltoledano/claude-task-master/discussions)
- 🐛 **Report Issues**: [GitHub Issues](https://github.com/eyaltoledano/claude-task-master/issues)
## 🎯 Tips for Best Results
### **Project Organization**
- Use descriptive task titles
- Add detailed implementation notes
- Set appropriate task dependencies
- Leverage AI features for complex tasks
### **Workflow Optimization**
- Review task details before starting work
- Use subtasks for complex features
- Update task status as you progress
- Add findings and learnings to task notes
### **Collaboration**
- Keep task descriptions updated
- Use consistent status conventions
- Document decisions in task details
- Share knowledge through task notes
---
## 🏆 Why Taskmaster Kanban?
**Visual workflow management** for your Taskmaster projects
**AI-powered task enhancement** built right in
**Real-time synchronization** keeps everything in sync
**Native VS Code integration** feels like part of the editor
**Free and open source** with active development
**Transform your development workflow today!** 🚀
---
*Originally Made with ❤️ by [David Maliglowka](https://x.com/DavidMaliglowka)*
## Support
This is an open-source project maintained in my spare time. While I strive to fix bugs and improve the extension, support is provided on a best-effort basis. Feel free to:
- Report issues on [GitHub](https://github.com/eyaltoledano/claude-task-master/issues)
- Submit pull requests with improvements
- Fork the project if you need specific modifications
## Disclaimer
This extension is provided "as is" without any warranties. Use at your own risk. The author is not responsible for any issues, data loss, or damages that may occur from using this extension. Please backup your work regularly and test thoroughly before using in important projects.

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 224 291" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M101.635 286.568L71.4839 256.414C65.6092 250.539 65.6092 241.03 71.4839 235.155L142.52 164.11C144.474 162.156 147.643 162.156 149.61 164.11L176.216 190.719C178.17 192.673 181.339 192.673 183.305 190.719L189.719 184.305C191.673 182.35 191.673 179.181 189.719 177.214L163.113 150.605C161.159 148.651 161.159 145.481 163.113 143.514L191.26 115.365C193.214 113.41 193.214 110.241 191.26 108.274L182.316 99.3291C180.362 97.3748 177.193 97.3748 175.226 99.3291L55.8638 218.706C49.989 224.581 40.4816 224.581 34.6068 218.706L4.4061 188.501C-1.4687 182.626 -1.4687 173.117 4.4061 167.242L23.8342 147.811C25.7883 145.857 25.7883 142.688 23.8342 140.721L4.78187 121.666C-1.09293 115.791 -1.09293 106.282 4.78187 100.406L34.7195 70.4527C40.5943 64.5772 50.1017 64.5772 55.9765 70.4527L75.555 90.0335C77.5091 91.9879 80.6782 91.9879 82.6448 90.0335L124.144 48.5292C126.098 46.5749 126.098 43.4054 124.144 41.4385L115.463 32.7568C113.509 30.8025 110.34 30.8025 108.374 32.7568L99.8683 41.2632C97.9143 43.2175 94.7451 43.2175 92.7785 41.2632L82.1438 30.6271C80.1897 28.6728 80.1897 25.5033 82.1438 23.5364L101.271 4.40662C107.146 -1.46887 116.653 -1.46887 122.528 4.40662L152.478 34.3604C158.353 40.2359 158.353 49.7444 152.478 55.6199L82.6323 125.474C80.6782 127.429 77.5091 127.429 75.5425 125.474L48.8741 98.8029C46.9201 96.8486 43.7509 96.8486 41.7843 98.8029L33.1036 107.485C31.1496 109.439 31.1496 112.608 33.1036 114.575L59.2458 140.721C61.1999 142.675 61.1999 145.844 59.2458 147.811L32.7404 174.32C30.7863 176.274 30.7863 179.444 32.7404 181.411L41.6841 190.355C43.6382 192.31 46.8073 192.31 48.7739 190.355L168.136 70.9789C174.011 65.1034 183.518 65.1034 189.393 70.9789L219.594 101.183C225.469 107.059 225.469 116.567 219.594 122.443L198.537 143.502C196.583 145.456 196.583 148.626 198.537 150.592L218.053 170.111C223.928 175.986 223.928 185.495 218.053 191.37L190.37 219.056C184.495 224.932 174.988 224.932 169.113 219.056L149.597 199.538C147.643 197.584 144.474 197.584 142.508 199.538L99.8057 242.245C97.8516 244.2 97.8516 247.369 99.8057 249.336L108.699 258.231C110.653 260.185 113.823 260.185 115.789 258.231L122.954 251.065C124.908 249.11 128.077 249.11 130.044 251.065L140.679 261.701C142.633 263.655 142.633 266.825 140.679 268.791L122.879 286.593C117.004 292.469 107.497 292.469 101.622 286.593L101.635 286.568Z" fill="#CCCCCC"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 224 291" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M101.635 286.568L71.4839 256.414C65.6092 250.539 65.6092 241.03 71.4839 235.155L142.52 164.11C144.474 162.156 147.643 162.156 149.61 164.11L176.216 190.719C178.17 192.673 181.339 192.673 183.305 190.719L189.719 184.305C191.673 182.35 191.673 179.181 189.719 177.214L163.113 150.605C161.159 148.651 161.159 145.481 163.113 143.514L191.26 115.365C193.214 113.41 193.214 110.241 191.26 108.274L182.316 99.3291C180.362 97.3748 177.193 97.3748 175.226 99.3291L55.8638 218.706C49.989 224.581 40.4816 224.581 34.6068 218.706L4.4061 188.501C-1.4687 182.626 -1.4687 173.117 4.4061 167.242L23.8342 147.811C25.7883 145.857 25.7883 142.688 23.8342 140.721L4.78187 121.666C-1.09293 115.791 -1.09293 106.282 4.78187 100.406L34.7195 70.4527C40.5943 64.5772 50.1017 64.5772 55.9765 70.4527L75.555 90.0335C77.5091 91.9879 80.6782 91.9879 82.6448 90.0335L124.144 48.5292C126.098 46.5749 126.098 43.4054 124.144 41.4385L115.463 32.7568C113.509 30.8025 110.34 30.8025 108.374 32.7568L99.8683 41.2632C97.9143 43.2175 94.7451 43.2175 92.7785 41.2632L82.1438 30.6271C80.1897 28.6728 80.1897 25.5033 82.1438 23.5364L101.271 4.40662C107.146 -1.46887 116.653 -1.46887 122.528 4.40662L152.478 34.3604C158.353 40.2359 158.353 49.7444 152.478 55.6199L82.6323 125.474C80.6782 127.429 77.5091 127.429 75.5425 125.474L48.8741 98.8029C46.9201 96.8486 43.7509 96.8486 41.7843 98.8029L33.1036 107.485C31.1496 109.439 31.1496 112.608 33.1036 114.575L59.2458 140.721C61.1999 142.675 61.1999 145.844 59.2458 147.811L32.7404 174.32C30.7863 176.274 30.7863 179.444 32.7404 181.411L41.6841 190.355C43.6382 192.31 46.8073 192.31 48.7739 190.355L168.136 70.9789C174.011 65.1034 183.518 65.1034 189.393 70.9789L219.594 101.183C225.469 107.059 225.469 116.567 219.594 122.443L198.537 143.502C196.583 145.456 196.583 148.626 198.537 150.592L218.053 170.111C223.928 175.986 223.928 185.495 218.053 191.37L190.37 219.056C184.495 224.932 174.988 224.932 169.113 219.056L149.597 199.538C147.643 197.584 144.474 197.584 142.508 199.538L99.8057 242.245C97.8516 244.2 97.8516 247.369 99.8057 249.336L108.699 258.231C110.653 260.185 113.823 260.185 115.789 258.231L122.954 251.065C124.908 249.11 128.077 249.11 130.044 251.065L140.679 261.701C142.633 263.655 142.633 266.825 140.679 268.791L122.879 286.593C117.004 292.469 107.497 292.469 101.622 286.593L101.635 286.568Z" fill="#424242"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 224 291" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M101.635 286.568L71.4839 256.414C65.6092 250.539 65.6092 241.03 71.4839 235.155L142.52 164.11C144.474 162.156 147.643 162.156 149.61 164.11L176.216 190.719C178.17 192.673 181.339 192.673 183.305 190.719L189.719 184.305C191.673 182.35 191.673 179.181 189.719 177.214L163.113 150.605C161.159 148.651 161.159 145.481 163.113 143.514L191.26 115.365C193.214 113.41 193.214 110.241 191.26 108.274L182.316 99.3291C180.362 97.3748 177.193 97.3748 175.226 99.3291L55.8638 218.706C49.989 224.581 40.4816 224.581 34.6068 218.706L4.4061 188.501C-1.4687 182.626 -1.4687 173.117 4.4061 167.242L23.8342 147.811C25.7883 145.857 25.7883 142.688 23.8342 140.721L4.78187 121.666C-1.09293 115.791 -1.09293 106.282 4.78187 100.406L34.7195 70.4527C40.5943 64.5772 50.1017 64.5772 55.9765 70.4527L75.555 90.0335C77.5091 91.9879 80.6782 91.9879 82.6448 90.0335L124.144 48.5292C126.098 46.5749 126.098 43.4054 124.144 41.4385L115.463 32.7568C113.509 30.8025 110.34 30.8025 108.374 32.7568L99.8683 41.2632C97.9143 43.2175 94.7451 43.2175 92.7785 41.2632L82.1438 30.6271C80.1897 28.6728 80.1897 25.5033 82.1438 23.5364L101.271 4.40662C107.146 -1.46887 116.653 -1.46887 122.528 4.40662L152.478 34.3604C158.353 40.2359 158.353 49.7444 152.478 55.6199L82.6323 125.474C80.6782 127.429 77.5091 127.429 75.5425 125.474L48.8741 98.8029C46.9201 96.8486 43.7509 96.8486 41.7843 98.8029L33.1036 107.485C31.1496 109.439 31.1496 112.608 33.1036 114.575L59.2458 140.721C61.1999 142.675 61.1999 145.844 59.2458 147.811L32.7404 174.32C30.7863 176.274 30.7863 179.444 32.7404 181.411L41.6841 190.355C43.6382 192.31 46.8073 192.31 48.7739 190.355L168.136 70.9789C174.011 65.1034 183.518 65.1034 189.393 70.9789L219.594 101.183C225.469 107.059 225.469 116.567 219.594 122.443L198.537 143.502C196.583 145.456 196.583 148.626 198.537 150.592L218.053 170.111C223.928 175.986 223.928 185.495 218.053 191.37L190.37 219.056C184.495 224.932 174.988 224.932 169.113 219.056L149.597 199.538C147.643 197.584 144.474 197.584 142.508 199.538L99.8057 242.245C97.8516 244.2 97.8516 247.369 99.8057 249.336L108.699 258.231C110.653 260.185 113.823 260.185 115.789 258.231L122.954 251.065C124.908 249.11 128.077 249.11 130.044 251.065L140.679 261.701C142.633 263.655 142.633 266.825 140.679 268.791L122.879 286.593C117.004 292.469 107.497 292.469 101.622 286.593L101.635 286.568Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +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"
}

View File

@@ -0,0 +1,222 @@
# VS Code Extension CI/CD Setup
This document explains the CI/CD setup for the Task Master VS Code extension using automated changesets.
## 🔄 Workflows Overview
### 1. Extension CI (`extension-ci.yml`)
#### Triggers
- Push to `main` or `next` branches (only when extension files change)
- Pull requests to `main` or `next` (only when extension files change)
#### What it does
- ✅ Lints and type-checks the extension code
- 🔨 Builds the extension (`npm run build`)
- 📦 Creates a clean package (`npm run package`)
- 🧪 Runs tests with VS Code test framework
- 📋 Creates a test VSIX package to verify packaging works
- 💾 Uploads build artifacts for inspection
### 2. Version & Publish (`version.yml`)
**Triggers:**
- Push to `main` branch
**What it does:**
- 🔍 Detects changeset files for pending releases
- 📝 Creates "Version Packages" PR with updated versions and CHANGELOG
- 🤖 When Version PR is merged, automatically:
- 🔨 Builds and packages the extension
- 🏷️ Creates git tags with changeset automation
- 📤 Publishes to VS Code Marketplace
- 🌍 Publishes to Open VSX Registry
- 📊 Updates package versions and CHANGELOG
## 🚀 Changeset Workflow
### Creating Changes
When making changes to the extension:
1. **Make your code changes**
2. **Create a changeset**:
```bash
# From project root
npx changeset add
```
3. **Select the extension package**: Choose `taskr-kanban` when prompted
4. **Select version bump type**:
- `patch`: Bug fixes, minor updates
- `minor`: New features, backwards compatible
- `major`: Breaking changes
5. **Write a summary**: Describe what changed for users
6. **Commit changeset file** along with your code changes
7. **Push to feature branch** and create PR
### Automated Publishing Process
1. **PR with changeset** gets merged to `main`
2. **Version workflow** detects changesets and creates "Version Packages" PR
3. **Review and merge** the Version PR
4. **Automated publishing** happens immediately:
- Extension is built using 3-file packaging system
- VSIX package is created and tested
- Published to VS Code Marketplace (if `VSCE_PAT` is set)
- Published to Open VSX Registry (if `OVSX_PAT` is set)
- Git tags are created: `taskr-kanban@1.0.1`
- CHANGELOG is updated automatically
## 🔑 Required Secrets
To use the automated publishing, you need to set up these GitHub repository secrets:
### `VSCE_PAT` (VS Code Marketplace Personal Access Token)
1. Go to [Azure DevOps](https://dev.azure.com/)
2. Sign in with your Microsoft account
3. Create a Personal Access Token:
- **Name**: VS Code Extension Publishing
- **Organization**: All accessible organizations
- **Expiration**: Custom (recommend 1 year)
- **Scopes**: Custom defined → **Marketplace** → **Manage**
4. Copy the token and add it to GitHub Secrets as `VSCE_PAT`
### `OVSX_PAT` (Open VSX Registry Personal Access Token)
1. Go to [Open VSX Registry](https://open-vsx.org/)
2. Sign in with your GitHub account
3. Go to your [User Settings](https://open-vsx.org/user-settings/tokens)
4. Create a new Access Token:
- **Description**: VS Code Extension Publishing
- **Scopes**: Leave default (full access)
5. Copy the token and add it to GitHub Secrets as `OVSX_PAT`
### `GITHUB_TOKEN` (automatically provided)
This is automatically available in GitHub Actions - no setup required.
## 📋 Version Management
### Changeset-Based Versioning
Versions are automatically managed by changesets:
- **No manual version updates needed** - changesets handle this automatically
- **Semantic versioning** is enforced based on changeset types
- **Changelog generation** happens automatically
- **Git tagging** is handled by the automation
### Critical Fields Sync
The automation ensures these fields stay in sync between `package.json` and `package.publish.json`:
```json
{
"version": "1.0.2", // ✅ AUTO-SYNCED
"publisher": "Hamster", // ⚠️ MUST MATCH MANUALLY
"displayName": "taskr: Task Master Kanban", // ⚠️ MUST MATCH MANUALLY
"description": "...", // ⚠️ MUST MATCH MANUALLY
"engines": { "vscode": "^1.93.0" }, // ⚠️ MUST MATCH MANUALLY
"categories": [...], // ⚠️ MUST MATCH MANUALLY
"contributes": { ... } // ⚠️ MUST MATCH MANUALLY
}
```
**Note**: Only `version` is automatically synced. Other fields must be manually kept in sync.
## 🔍 Monitoring Builds
### CI Status
- **Green ✅**: Extension builds and tests successfully
- **Red ❌**: Build/test failures - check logs for details
- **Yellow 🟡**: Partial success - some jobs may have warnings
### Version PR Status
- **Version PR Created**: Changesets detected, review and merge to publish
- **No Version PR**: No changesets found, no releases pending
- **Version PR Merged**: Automated publishing triggered
### Release Status
- **Published 🎉**: Extension live on VS Code Marketplace and Open VSX
- **Skipped **: No changesets found, no release needed
- **Failed ❌**: Check logs - often missing secrets or build issues
### Artifacts
Workflows upload artifacts that you can download:
- **CI**: Test results, built files, and VSIX package
- **Version**: Final VSIX package and published extension
## 🛠️ Troubleshooting
### Common Issues
#### No Version PR Created
- **Check**: Changeset files exist in `.changeset/` directory
- **Check**: Changeset refers to `taskr-kanban` package name
- **Check**: Changes were pushed to `main` branch
- **Solution**: Create changeset with `npx changeset add`
#### Version PR Not Publishing
- **Check**: Version PR was actually merged (not just closed)
- **Check**: Required secrets (`VSCE_PAT`, `OVSX_PAT`) are set
- **Check**: No build failures in workflow logs
- **Solution**: Re-run workflow or check secret configuration
#### `VSCE_PAT` is not set Error
- Ensure `VSCE_PAT` secret is added to repository
- Check token hasn't expired
- Verify token has Marketplace → Manage permissions
#### `OVSX_PAT` is not set Error
- Ensure `OVSX_PAT` secret is added to repository
- Check token hasn't expired
- Verify you're signed in to Open VSX Registry with GitHub
#### Build Failures
- Check extension code compiles locally: `cd apps/extension && npm run build`
- Verify tests pass locally: `npm run test`
- Check for TypeScript errors: `npm run check-types`
#### Packaging Failures
- Ensure clean package builds: `npm run package`
- Check vsix-build structure is correct
- Verify `package.publish.json` has correct `repository` field
#### Changeset Issues
- **Wrong package name**: Ensure changeset refers to `taskr-kanban`
- **Invalid format**: Check changeset markdown format is correct
- **Merge conflicts**: Resolve any conflicts in changeset files
## 📁 File Structure Impact
The CI workflows respect the 3-file packaging system:
- **Development**: Uses `package.json` for dependencies and scripts
- **Release**: Uses `package.publish.json` for clean marketplace package
- **Build**: Uses `package.mjs` to create `vsix-build/` for final packaging
- **Changesets**: Automatically manage versions across the system
## 🌍 Dual Registry Publishing
Your extension will be automatically published to both:
- **VS Code Marketplace** - For official VS Code users
- **Open VSX Registry** - For Cursor, Windsurf, VSCodium, Gitpod, Eclipse Theia, and other compatible editors
## 🎯 Benefits of Changeset Automation
-**Automated versioning**: No manual version bumps needed
-**Generated changelogs**: Automatic, accurate release notes
-**Semantic versioning**: Enforced through changeset types
-**Git tagging**: Proper tags for extension releases
-**Conflict prevention**: Clear separation of extension vs. main package versions
-**Review process**: Version changes are reviewable via PR
-**Rollback capability**: Easy to revert if issues arise
This ensures clean, predictable, and fully automated publishing to both registries! 🚀

View File

@@ -0,0 +1,256 @@
# VS Code Extension Development Guide
## 📁 File Structure Overview
This VS Code extension uses a **3-file packaging system** to avoid dependency conflicts during publishing:
```
apps/extension/
├── package.json # Development configuration
├── package.publish.json # Clean publishing configuration
├── package.mjs # Build script for packaging
├── .vscodeignore # Files to exclude from extension package
└── vsix-build/ # Generated clean package directory
```
## 📋 File Purposes
### `package.json` (Development)
- **Purpose**: Development environment with all build tools
- **Contains**:
- All `devDependencies` needed for building
- Development scripts (`build`, `watch`, `lint`, etc.)
- Development package name: `"taskr"`
- **Used for**: Local development, building, testing
### `package.publish.json` (Publishing)
- **Purpose**: Clean distribution version for VS Code Marketplace
- **Contains**:
- **No devDependencies** (avoids dependency conflicts)
- Publishing metadata (`keywords`, `repository`, `categories`)
- Marketplace package name: `"taskr-kanban"`
- VS Code extension configuration
- **Used for**: Final extension packaging
### `package.mjs` (Build Script)
- **Purpose**: Creates clean package for distribution
- **Process**:
1. Builds the extension (`build:js` + `build:css`)
2. Creates clean `vsix-build/` directory
3. Copies only essential files (no source code)
4. Renames `package.publish.json``package.json`
5. Ready for `vsce package`
## 🚀 Development Workflow
### Local Development
```bash
# Install dependencies
npm install
# Start development with hot reload
npm run watch
# Run just JavaScript build
npm run build:js
# Run just CSS build
npm run build:css
# Full production build
npm run build
# Type checking
npm run check-types
# Linting
npm run lint
```
### Testing in VS Code
1. Press `F5` in VS Code to launch Extension Development Host
2. Test your extension functionality in the new window
3. Use `Developer: Reload Window` to reload after changes
## 📦 Production Packaging
### Step 1: Build Clean Package
```bash
npm run package
```
This creates `vsix-build/` with clean distribution files.
### Step 2: Create VSIX
```bash
cd vsix-build
npx vsce package --no-dependencies
```
Creates: `taskr-kanban-1.0.1.vsix`
### Alternative: One Command
```bash
npm run package && cd vsix-build && npx vsce package --no-dependencies
```
## 🔄 Keeping Files in Sync
### Critical Fields to Sync Between Files
When updating extension metadata, ensure these fields match between `package.json` and `package.publish.json`:
#### Version & Identity
```json
{
"version": "1.0.1", // ⚠️ MUST MATCH
"publisher": "Hamster", // ⚠️ MUST MATCH
"displayName": "taskr: Task Master Kanban", // ⚠️ MUST MATCH
"description": "A visual Kanban board...", // ⚠️ MUST MATCH
}
```
#### VS Code Configuration
```json
{
"engines": { "vscode": "^1.101.0" }, // ⚠️ MUST MATCH
"categories": [...], // ⚠️ MUST MATCH
"activationEvents": [...], // ⚠️ MUST MATCH
"main": "./dist/extension.js", // ⚠️ MUST MATCH
"contributes": { ... } // ⚠️ MUST MATCH EXACTLY
}
```
### Key Differences (Should NOT Match)
```json
// package.json (dev)
{
"name": "taskr", // ✅ Short dev name
"devDependencies": { ... }, // ✅ Only in dev file
"scripts": { ... } // ✅ Build scripts
}
// package.publish.json (publishing)
{
"name": "taskr-kanban", // ✅ Marketplace name
"keywords": [...], // ✅ Only in publish file
"repository": "https://github.com/...", // ✅ Only in publish file
// NO devDependencies // ✅ Clean for publishing
// NO build scripts // ✅ Not needed in package
}
```
## 🤖 Automated Release Process
### Changesets Workflow
This extension uses [Changesets](https://github.com/changesets/changesets) for automated version management and publishing.
#### Adding Changes
When making changes to the extension:
1. **Make your code changes**
2. **Create a changeset**:
```bash
# From project root
npx changeset add
```
3. **Select the extension package**: Choose `taskr-kanban` when prompted
4. **Select version bump type**:
- `patch`: Bug fixes, minor updates
- `minor`: New features, backwards compatible
- `major`: Breaking changes
5. **Write a summary**: Describe what changed for users
#### Automated Publishing
The automation workflow runs on pushes to `main`:
1. **Version Workflow** (`.github/workflows/version.yml`):
- Detects when changesets exist
- Creates a "Version Packages" PR with updated versions and CHANGELOG
- When the PR is merged, automatically publishes the extension
2. **Release Process** (`scripts/release.sh`):
- Builds the extension using the 3-file packaging system
- Creates VSIX package
- Publishes to VS Code Marketplace (if `VSCE_PAT` is set)
- Publishes to Open VSX Registry (if `OVSX_PAT` is set)
- Creates git tags for the extension version
#### Required Secrets
For automated publishing, these secrets must be set in the repository:
- `VSCE_PAT`: Personal Access Token for VS Code Marketplace
- `OVSX_PAT`: Personal Access Token for Open VSX Registry
- `GITHUB_TOKEN`: Automatically provided by GitHub Actions
#### Manual Release
If needed, you can manually trigger a release:
```bash
# From project root
./scripts/release.sh
```
### Extension Tagging
The extension uses a separate tagging strategy from the main package:
- **Extension tags**: `taskr-kanban@1.0.1`
- **Main package tags**: `task-master-ai@2.1.0`
This allows independent versioning and prevents conflicts in the monorepo.
## 🔍 Troubleshooting
### Dependency Conflicts
**Problem**: `vsce package` fails with missing dependencies
**Solution**: Use the 3-file system - never run `vsce package` from root
### Build Failures
**Problem**: Extension not working after build
**Check**:
1. All files copied to `vsix-build/dist/`
2. `package.publish.json` has correct `main` field
3. VS Code engine version compatibility
### Sync Issues
**Problem**: Extension works locally but fails when packaged
**Check**: Ensure critical fields are synced between package files
### Changeset Issues
**Problem**: Version workflow not triggering
**Check**:
1. Changeset files exist in `.changeset/`
2. Package name in changeset matches `package.publish.json`
3. Changes are pushed to `main` branch
**Problem**: Publishing fails
**Check**:
1. Required secrets are set in repository settings
2. `package.publish.json` has correct repository URL
3. Build process completes successfully
## 📝 Version Release Checklist
### Manual Releases
1. **Create changeset**: `npx changeset add`
2. **Update critical fields** in both `package.json` and `package.publish.json`
3. **Test locally** with `F5` in VS Code
4. **Commit and push** to trigger automated workflow
### Automated Releases (Recommended)
1. **Create changeset**: `npx changeset add`
2. **Push to feature branch** and create PR
3. **Merge PR** - this triggers version PR creation
4. **Review and merge version PR** - this triggers automated publishing
## 🎯 Why This System?
- **Avoids dependency conflicts**: VS Code doesn't see dev dependencies
- **Clean distribution**: Only essential files in final package
- **Faster packaging**: No dependency resolution during `vsce package`
- **Maintainable**: Clear separation of dev vs. production configs
- **Reliable**: Consistent, conflict-free packaging process
- **Automated**: Changesets handle versioning and publishing automatically
- **Traceable**: Clear changelog and git tags for every release
---
**Remember**: Always use `npx changeset add` for changes, then push to trigger automated releases! 🚀

173
apps/extension/esbuild.js Normal file
View File

@@ -0,0 +1,173 @@
const esbuild = require('esbuild');
const path = require('path');
const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: 'esbuild-problem-matcher',
setup(build) {
build.onStart(() => {
console.log('[watch] build started');
});
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`);
console.error(
` ${location.file}:${location.line}:${location.column}:`
);
});
console.log('[watch] build finished');
});
}
};
/**
* @type {import('esbuild').Plugin}
*/
const aliasPlugin = {
name: 'alias',
setup(build) {
// Handle @/ aliases for shadcn/ui
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;
if (fs.existsSync(fullPath)) {
return { path: fullPath };
}
}
// Check if it's a directory with index file
for (const ext of extensions) {
const indexPath = path.join(resolvedPath, 'index' + ext);
if (fs.existsSync(indexPath)) {
return { path: indexPath };
}
}
// Fallback to original behavior
return { path: resolvedPath };
});
}
};
async function main() {
// Build configuration for the VS Code extension
const extensionCtx = await esbuild.context({
entryPoints: ['src/extension.ts'],
bundle: true,
format: 'cjs',
minify: production,
sourcemap: !production ? 'inline' : false,
sourcesContent: !production,
platform: 'node',
outdir: 'dist',
external: ['vscode'],
logLevel: 'silent',
// Add production optimizations
...(production && {
drop: ['debugger'],
pure: ['console.log', 'console.debug', 'console.trace']
}),
plugins: [esbuildProblemMatcherPlugin, aliasPlugin]
});
// Build configuration for the React webview
const webviewCtx = await esbuild.context({
entryPoints: ['src/webview/index.tsx'],
bundle: true,
format: 'iife',
globalName: 'App',
minify: production,
sourcemap: !production ? 'inline' : false,
sourcesContent: !production,
platform: 'browser',
outdir: 'dist',
logLevel: 'silent',
target: ['es2020'],
jsx: 'automatic',
jsxImportSource: 'react',
external: ['*.css'],
// Bundle React with webview since it's not available in the runtime
// This prevents the multiple React instances issue
// Ensure React is resolved from the workspace root to avoid duplicates
alias: {
react: path.resolve(__dirname, 'node_modules/react'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
},
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',
global: 'globalThis'
},
// Add production optimizations for webview too
...(production && {
drop: ['debugger'],
pure: ['console.log', 'console.debug', 'console.trace']
}),
plugins: [esbuildProblemMatcherPlugin, aliasPlugin]
});
// Build configuration for the React sidebar
const sidebarCtx = await esbuild.context({
entryPoints: ['src/webview/sidebar.tsx'],
bundle: true,
format: 'iife',
globalName: 'SidebarApp',
minify: production,
sourcemap: !production ? 'inline' : false,
sourcesContent: !production,
platform: 'browser',
outdir: 'dist',
logLevel: 'silent',
target: ['es2020'],
jsx: 'automatic',
jsxImportSource: 'react',
external: ['*.css'],
alias: {
react: path.resolve(__dirname, 'node_modules/react'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
},
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',
global: 'globalThis'
},
...(production && {
drop: ['debugger'],
pure: ['console.log', 'console.debug', 'console.trace']
}),
plugins: [esbuildProblemMatcherPlugin, aliasPlugin]
});
if (watch) {
await Promise.all([
extensionCtx.watch(),
webviewCtx.watch(),
sidebarCtx.watch()
]);
} else {
await Promise.all([
extensionCtx.rebuild(),
webviewCtx.rebuild(),
sidebarCtx.rebuild()
]);
await extensionCtx.dispose();
await webviewCtx.dispose();
await sidebarCtx.dispose();
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -1,16 +1,281 @@
{ {
"name": "extension", "name": "extension",
"private": true, "private": true,
"version": "0.20.0", "displayName": "TaskMaster",
"main": "index.js", "description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"scripts": { "version": "0.23.0",
"test": "echo \"Error: no test specified\" && exit 1" "publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {
"vscode": "^1.93.0"
},
"categories": ["AI", "Visualization", "Education", "Other"],
"main": "./dist/extension.js",
"activationEvents": ["onStartupFinished", "workspaceContains:.taskmaster/**"],
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "taskmaster",
"title": "TaskMaster",
"icon": "assets/sidebar-icon.svg"
}
]
},
"views": {
"taskmaster": [
{
"id": "taskmaster.welcome",
"name": "TaskMaster",
"type": "webview"
}
]
},
"commands": [
{
"command": "tm.showKanbanBoard",
"title": "TaskMaster: Show Board",
"icon": "$(checklist)"
},
{
"command": "tm.checkConnection",
"title": "TaskMaster: Check Connection"
},
{
"command": "tm.reconnect",
"title": "TaskMaster: Reconnect"
},
{
"command": "tm.openSettings",
"title": "TaskMaster: Open Settings"
}
],
"menus": {
"view/title": [
{
"command": "tm.showKanbanBoard",
"when": "view == taskmaster.welcome",
"group": "navigation"
}
]
},
"configuration": {
"title": "TaskMaster 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": ["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 TaskMaster MCP server (defaults to workspace root)"
},
"taskmaster.mcp.env": {
"type": "object",
"description": "Environment variables for the TaskMaster 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.mcp.requestTimeoutMs": {
"type": "number",
"default": 300000,
"minimum": 30000,
"maximum": 600000,
"description": "MCP request timeout in milliseconds (default: 5 minutes)"
},
"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": "npm run build",
"build": "npm run build:js && npm run build:css",
"build:js": "node ./esbuild.js --production",
"build:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --minify",
"package": "npm exec node ./package.mjs",
"package:direct": "node ./package.mjs",
"debug:env": "node ./debug-env.mjs",
"compile": "node ./esbuild.js",
"watch": "npm run watch:js & npm run watch:css",
"watch:js": "node ./esbuild.js --watch",
"watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
"check-types": "tsc --noEmit"
}, },
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": { "devDependencies": {
"typescript": "^5.8.3" "@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",
"@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",
"fs-extra": "^11.3.0",
"lucide-react": "^0.525.0",
"npm-run-all": "^4.1.5",
"postcss": "8.5.6",
"tailwind-merge": "^3.3.1",
"tailwindcss": "4.1.11",
"typescript": "^5.8.3",
"@tanstack/react-query": "^5.83.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"overrides": {
"glob@<8": "^10.4.5",
"inflight": "npm:@tootallnate/once@2"
} }
} }

136
apps/extension/package.mjs Normal file
View File

@@ -0,0 +1,136 @@
import { execSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs-extra';
// --- Configuration ---
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageDir = path.resolve(__dirname, 'vsix-build');
// --- End Configuration ---
try {
console.log('🚀 Starting packaging process...');
// 1. Build Project
console.log('\nBuilding JavaScript...');
execSync('npm run build:js', { stdio: 'inherit' });
console.log('\nBuilding CSS...');
execSync('npm run build:css', { stdio: 'inherit' });
// 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', 'sidebar.js'];
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}`);
}
}
// 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}`);
}
}
// 5. Sync versions and prepare the final package.json
console.log('Syncing versions and preparing the final package.json...');
// Read current versions
const devPackagePath = path.resolve(__dirname, 'package.json');
const publishPackagePath = path.resolve(__dirname, 'package.publish.json');
const devPackage = JSON.parse(fs.readFileSync(devPackagePath, 'utf8'));
const publishPackage = JSON.parse(
fs.readFileSync(publishPackagePath, 'utf8')
);
// Check if versions are in sync
if (devPackage.version !== publishPackage.version) {
console.log(
` - Version sync needed: ${publishPackage.version}${devPackage.version}`
);
publishPackage.version = devPackage.version;
// Update the source package.publish.json file
fs.writeFileSync(
publishPackagePath,
JSON.stringify(publishPackage, null, '\t') + '\n'
);
console.log(
` - Updated package.publish.json version to ${devPackage.version}`
);
} else {
console.log(` - Versions already in sync: ${devPackage.version}`);
}
// Copy the (now synced) package.publish.json as package.json
fs.copySync(publishPackagePath, path.resolve(packageDir, 'package.json'));
console.log(' - Copied package.publish.json as package.json');
// 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');
}
// 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');
}
// 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');
}
// Small delay to ensure file system operations complete
await new Promise((resolve) => setTimeout(resolve, 100));
// 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 && npx vsce package --no-dependencies`
);
// Use the synced version for output
const finalVersion = devPackage.version;
console.log(
`\nYour extension will be packaged to: vsix-build/task-master-${finalVersion}.vsix`
);
} catch (error) {
console.error('\n❌ Packaging failed!');
console.error(error.message);
process.exit(1);
}

View File

@@ -0,0 +1,250 @@
{
"name": "task-master-hamster",
"displayName": "Taskmaster AI",
"description": "A visual Kanban board interface for Taskmaster projects in VS Code",
"version": "0.22.3",
"publisher": "Hamster",
"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": ["onStartupFinished", "workspaceContains:.taskmaster/**"],
"main": "./dist/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "taskmaster",
"title": "Taskmaster",
"icon": "assets/sidebar-icon.svg"
}
]
},
"views": {
"taskmaster": [
{
"id": "taskmaster.welcome",
"name": "Taskmaster",
"type": "webview"
}
]
},
"commands": [
{
"command": "tm.showKanbanBoard",
"title": "Taskmaster: Show Board"
},
{
"command": "tm.checkConnection",
"title": "Taskmaster: Check Connection"
},
{
"command": "tm.reconnect",
"title": "Taskmaster: Reconnect"
},
{
"command": "tm.openSettings",
"title": "Taskmaster: Open Settings"
}
],
"configuration": {
"title": "Taskmaster 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.mcp.requestTimeoutMs": {
"type": "number",
"default": 300000,
"minimum": 30000,
"maximum": 600000,
"description": "MCP request timeout in milliseconds (default: 5 minutes)"
},
"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"
}
}
}
}
}

View File

@@ -0,0 +1,291 @@
import { ArrowLeft, RefreshCw, Settings } from 'lucide-react';
import type React from 'react';
import { useEffect, useState, useCallback } from 'react';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from './ui/card';
import { ScrollArea } from './ui/scroll-area';
import { Separator } from './ui/separator';
interface ModelConfig {
provider: string;
modelId: string;
maxTokens: number;
temperature: number;
}
interface ConfigData {
models?: {
main?: ModelConfig;
research?: ModelConfig;
fallback?: ModelConfig;
};
global?: {
defaultNumTasks?: number;
defaultSubtasks?: number;
defaultPriority?: string;
projectName?: string;
responseLanguage?: string;
};
}
interface ConfigViewProps {
sendMessage: (message: any) => Promise<any>;
onNavigateBack: () => void;
}
export const ConfigView: React.FC<ConfigViewProps> = ({
sendMessage,
onNavigateBack
}) => {
const [config, setConfig] = useState<ConfigData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadConfig = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await sendMessage({ type: 'getConfig' });
setConfig(response);
} catch (err) {
setError('Failed to load configuration');
console.error('Error loading config:', err);
} finally {
setLoading(false);
}
}, [sendMessage]);
useEffect(() => {
loadConfig();
}, [loadConfig]);
const modelLabels = {
main: {
label: 'Main Model',
icon: '🤖',
description: 'Primary model for task generation'
},
research: {
label: 'Research Model',
icon: '🔍',
description: 'Model for research-backed operations'
},
fallback: {
label: 'Fallback Model',
icon: '🔄',
description: 'Backup model if primary fails'
}
};
return (
<div className="flex flex-col h-full bg-vscode-editor-background">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-vscode-border">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={onNavigateBack}
className="h-8 w-8"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2">
<Settings className="w-5 h-5" />
<h1 className="text-lg font-semibold">Task Master Configuration</h1>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={loadConfig}
className="h-8 w-8"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
{/* Content */}
<ScrollArea className="flex-1 overflow-hidden">
<div className="p-6 pb-12">
{loading ? (
<div className="flex items-center justify-center py-8">
<RefreshCw className="w-6 h-6 animate-spin text-vscode-foreground/50" />
</div>
) : error ? (
<div className="text-red-500 text-center py-8">{error}</div>
) : config ? (
<div className="space-y-6 max-w-4xl mx-auto">
{/* Models Section */}
<Card>
<CardHeader>
<CardTitle>AI Models</CardTitle>
<CardDescription>
Models configured for different Task Master operations
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{config.models &&
Object.entries(config.models).map(([key, modelConfig]) => {
const label =
modelLabels[key as keyof typeof modelLabels];
if (!label || !modelConfig) return null;
return (
<div key={key} className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-lg">{label.icon}</span>
<div>
<h4 className="font-medium">{label.label}</h4>
<p className="text-xs text-vscode-foreground/60">
{label.description}
</p>
</div>
</div>
<div className="bg-vscode-input/20 rounded-md p-3 space-y-1">
<div className="flex justify-between">
<span className="text-sm text-vscode-foreground/80">
Provider:
</span>
<Badge variant="secondary">
{modelConfig.provider}
</Badge>
</div>
<div className="flex justify-between">
<span className="text-sm text-vscode-foreground/80">
Model:
</span>
<code className="text-xs font-mono bg-vscode-input/30 px-2 py-1 rounded">
{modelConfig.modelId}
</code>
</div>
<div className="flex justify-between">
<span className="text-sm text-vscode-foreground/80">
Max Tokens:
</span>
<span className="text-sm">
{modelConfig.maxTokens.toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-vscode-foreground/80">
Temperature:
</span>
<span className="text-sm">
{modelConfig.temperature}
</span>
</div>
</div>
</div>
);
})}
</CardContent>
</Card>
{/* Task Defaults Section */}
{config.global && (
<Card>
<CardHeader>
<CardTitle>Task Defaults</CardTitle>
<CardDescription>
Default values for new tasks and subtasks
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Default Number of Tasks
</span>
<Badge variant="outline">
{config.global.defaultNumTasks || 10}
</Badge>
</div>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Default Number of Subtasks
</span>
<Badge variant="outline">
{config.global.defaultSubtasks || 5}
</Badge>
</div>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Default Priority
</span>
<Badge
variant={
config.global.defaultPriority === 'high'
? 'destructive'
: config.global.defaultPriority === 'low'
? 'secondary'
: 'default'
}
>
{config.global.defaultPriority || 'medium'}
</Badge>
</div>
{config.global.projectName && (
<>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Project Name
</span>
<span className="text-sm text-vscode-foreground/80">
{config.global.projectName}
</span>
</div>
</>
)}
{config.global.responseLanguage && (
<>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Response Language
</span>
<span className="text-sm text-vscode-foreground/80">
{config.global.responseLanguage}
</span>
</div>
</>
)}
</div>
</CardContent>
</Card>
)}
{/* Info Card */}
<Card>
<CardContent className="pt-6">
<p className="text-sm text-vscode-foreground/60">
To modify these settings, go to{' '}
<code className="bg-vscode-input/30 px-1 py-0.5 rounded">
.taskmaster/config.json
</code>{' '}
and modify them, or use the MCP.
</p>
</CardContent>
</Card>
</div>
) : (
<div className="text-center py-8 text-vscode-foreground/50">
No configuration found. Please run `task-master init` in your
project.
</div>
)}
</div>
</ScrollArea>
</div>
);
};

View File

@@ -0,0 +1,380 @@
import type React from 'react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
import {
Wand2,
Loader2,
PlusCircle,
TrendingUp,
TrendingDown
} from 'lucide-react';
import {
useUpdateTask,
useUpdateSubtask,
useScopeUpTask,
useScopeDownTask
} from '../../webview/hooks/useTaskQueries';
import type { TaskMasterTask } from '../../webview/types';
interface AIActionsSectionProps {
currentTask: TaskMasterTask;
isSubtask: boolean;
parentTask?: TaskMasterTask | null;
sendMessage: (message: any) => Promise<any>;
refreshComplexityAfterAI: () => void;
onRegeneratingChange?: (isRegenerating: boolean) => void;
onAppendingChange?: (isAppending: boolean) => void;
}
export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
currentTask,
isSubtask,
parentTask,
sendMessage,
refreshComplexityAfterAI,
onRegeneratingChange,
onAppendingChange
}) => {
const [prompt, setPrompt] = useState('');
const [scopePrompt, setScopePrompt] = useState('');
const [scopeStrength, setScopeStrength] = useState<
'light' | 'regular' | 'heavy'
>('regular');
const [lastAction, setLastAction] = useState<
'regenerate' | 'append' | 'scope-up' | 'scope-down' | null
>(null);
const updateTask = useUpdateTask();
const updateSubtask = useUpdateSubtask();
const scopeUpTask = useScopeUpTask();
const scopeDownTask = useScopeDownTask();
const handleRegenerate = async () => {
if (!currentTask || !prompt.trim()) {
return;
}
setLastAction('regenerate');
onRegeneratingChange?.(true);
try {
if (isSubtask && parentTask) {
await updateSubtask.mutateAsync({
taskId: `${parentTask.id}.${currentTask.id}`,
prompt: prompt,
options: { research: false }
});
} else {
await updateTask.mutateAsync({
taskId: currentTask.id,
updates: { description: prompt },
options: { append: false, research: false }
});
}
setPrompt('');
refreshComplexityAfterAI();
} catch (error) {
console.error('❌ TaskDetailsView: Failed to regenerate task:', error);
} finally {
setLastAction(null);
onRegeneratingChange?.(false);
}
};
const handleAppend = async () => {
if (!currentTask || !prompt.trim()) {
return;
}
setLastAction('append');
onAppendingChange?.(true);
try {
if (isSubtask && parentTask) {
await updateSubtask.mutateAsync({
taskId: `${parentTask.id}.${currentTask.id}`,
prompt: prompt,
options: { research: false }
});
} else {
await updateTask.mutateAsync({
taskId: currentTask.id,
updates: { description: prompt },
options: { append: true, research: false }
});
}
setPrompt('');
refreshComplexityAfterAI();
} catch (error) {
console.error('❌ TaskDetailsView: Failed to append to task:', error);
} finally {
setLastAction(null);
onAppendingChange?.(false);
}
};
const handleScopeUp = async () => {
if (!currentTask) {
return;
}
setLastAction('scope-up');
try {
const taskId =
isSubtask && parentTask
? `${parentTask.id}.${currentTask.id}`
: currentTask.id;
await scopeUpTask.mutateAsync({
taskId,
strength: scopeStrength,
prompt: scopePrompt.trim() || undefined,
options: { research: false }
});
setScopePrompt('');
refreshComplexityAfterAI();
} catch (error) {
console.error('❌ AIActionsSection: Failed to scope up task:', error);
} finally {
setLastAction(null);
}
};
const handleScopeDown = async () => {
if (!currentTask) {
return;
}
setLastAction('scope-down');
try {
const taskId =
isSubtask && parentTask
? `${parentTask.id}.${currentTask.id}`
: currentTask.id;
await scopeDownTask.mutateAsync({
taskId,
strength: scopeStrength,
prompt: scopePrompt.trim() || undefined,
options: { research: false }
});
setScopePrompt('');
refreshComplexityAfterAI();
} catch (error) {
console.error('❌ AIActionsSection: Failed to scope down task:', error);
} finally {
setLastAction(null);
}
};
// Track loading states based on the last action
const isLoading =
updateTask.isPending ||
updateSubtask.isPending ||
scopeUpTask.isPending ||
scopeDownTask.isPending;
const isRegenerating = isLoading && lastAction === 'regenerate';
const isAppending = isLoading && lastAction === 'append';
const isScopingUp = isLoading && lastAction === 'scope-up';
const isScopingDown = isLoading && lastAction === 'scope-down';
return (
<CollapsibleSection
title="AI Actions"
icon={Wand2}
defaultExpanded={true}
buttonClassName="text-vscode-foreground/80 hover:text-vscode-foreground"
>
<div className="space-y-6">
{/* Standard AI Actions Section */}
<div className="space-y-4">
<div>
<Label
htmlFor="ai-prompt"
className="block text-sm font-medium text-vscode-foreground/80 mb-2"
>
Enter your prompt
</Label>
<Textarea
id="ai-prompt"
placeholder={
isSubtask
? 'Describe implementation notes, progress updates, or findings to add to this subtask...'
: 'Describe what you want to change or add to this task...'
}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="min-h-[100px] bg-vscode-input-background border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
disabled={isLoading}
/>
</div>
<div className="flex gap-3">
{!isSubtask && (
<Button
onClick={handleRegenerate}
disabled={!prompt.trim() || isLoading}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isRegenerating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Regenerating...
</>
) : (
<>
<Wand2 className="w-4 h-4 mr-2" />
Regenerate Task
</>
)}
</Button>
)}
<Button
onClick={handleAppend}
disabled={!prompt.trim() || isLoading}
variant={isSubtask ? 'default' : 'outline'}
className={
isSubtask
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: 'bg-secondary text-secondary-foreground hover:bg-secondary/90 border-widget-border'
}
>
{isAppending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{isSubtask ? 'Updating...' : 'Appending...'}
</>
) : (
<>
<PlusCircle className="w-4 h-4 mr-2" />
{isSubtask ? 'Add Notes to Subtask' : 'Append to Task'}
</>
)}
</Button>
</div>
</div>
{/* Scope Adjustment Section */}
<div className="border-t border-vscode-widget-border pt-4 space-y-4">
<div>
<Label className="block text-sm font-medium text-vscode-foreground/80 mb-3">
Task Complexity Adjustment
</Label>
{/* Strength Selection */}
<div className="mb-3">
<Label className="block text-xs text-vscode-foreground/60 mb-2">
Adjustment Strength
</Label>
<div className="flex gap-2">
{(['light', 'regular', 'heavy'] as const).map((strength) => (
<Button
key={strength}
onClick={() => setScopeStrength(strength)}
variant={scopeStrength === strength ? 'default' : 'outline'}
size="sm"
className={
scopeStrength === strength
? 'bg-accent text-accent-foreground border-accent'
: 'border-widget-border text-vscode-foreground/80 hover:bg-vscode-list-hoverBackground'
}
disabled={isLoading}
>
{strength.charAt(0).toUpperCase() + strength.slice(1)}
</Button>
))}
</div>
</div>
{/* Scope Prompt */}
<Textarea
placeholder="Optional: Specify how to adjust complexity (e.g., 'Focus on error handling', 'Remove unnecessary details', 'Add more implementation steps')"
value={scopePrompt}
onChange={(e) => setScopePrompt(e.target.value)}
className="min-h-[80px] bg-vscode-input-background border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
disabled={isLoading}
/>
</div>
<div className="flex gap-3">
<Button
onClick={handleScopeUp}
disabled={isLoading}
variant="outline"
className="flex-1 border-green-600/50 text-green-400 hover:bg-green-600/10 hover:border-green-500"
>
{isScopingUp ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Scoping Up...
</>
) : (
<>
<TrendingUp className="w-4 h-4 mr-2" />
Scope Up
</>
)}
</Button>
<Button
onClick={handleScopeDown}
disabled={isLoading}
variant="outline"
className="flex-1 border-blue-600/50 text-blue-400 hover:bg-blue-600/10 hover:border-blue-500"
>
{isScopingDown ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Scoping Down...
</>
) : (
<>
<TrendingDown className="w-4 h-4 mr-2" />
Scope Down
</>
)}
</Button>
</div>
</div>
{/* Help Text */}
<div className="text-xs text-vscode-foreground/60 space-y-1">
{isSubtask ? (
<p>
<strong>Add Notes:</strong> Appends timestamped implementation
notes, progress updates, or findings to this subtask's details
</p>
) : (
<>
<p>
<strong>Regenerate:</strong> Completely rewrites the task
description and subtasks based on your prompt
</p>
<p>
<strong>Append:</strong> Adds new content to the existing task
implementation details based on your prompt
</p>
</>
)}
<p>
<strong>Scope Up:</strong> Increases task complexity with more
details, requirements, or implementation steps
</p>
<p>
<strong>Scope Down:</strong> Decreases task complexity by
simplifying or removing unnecessary details
</p>
</div>
</div>
</CollapsibleSection>
);
};

View File

@@ -0,0 +1,204 @@
import type React from 'react';
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
interface MarkdownRendererProps {
content: string;
className?: string;
}
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
content,
className = ''
}) => {
const parseMarkdown = (text: string) => {
const parts = [];
const lines = text.split('\n');
let currentBlock = [];
let inCodeBlock = false;
let codeLanguage = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('```')) {
if (inCodeBlock) {
if (currentBlock.length > 0) {
parts.push({
type: 'code',
content: currentBlock.join('\n'),
language: codeLanguage
});
currentBlock = [];
}
inCodeBlock = false;
codeLanguage = '';
} else {
if (currentBlock.length > 0) {
parts.push({
type: 'text',
content: currentBlock.join('\n')
});
currentBlock = [];
}
inCodeBlock = true;
codeLanguage = line.substring(3).trim();
}
} else {
currentBlock.push(line);
}
}
if (currentBlock.length > 0) {
parts.push({
type: inCodeBlock ? 'code' : 'text',
content: currentBlock.join('\n'),
language: codeLanguage
});
}
return parts;
};
const parts = parseMarkdown(content);
return (
<div className={className}>
{parts.map((part, index) => {
if (part.type === 'code') {
return (
<pre
key={index}
className="bg-vscode-editor-background rounded-md p-4 overflow-x-auto mb-4 border border-vscode-editor-lineHighlightBorder"
>
<code className="text-sm text-vscode-editor-foreground font-mono">
{part.content}
</code>
</pre>
);
}
return (
<div key={index} className="whitespace-pre-wrap mb-4 last:mb-0">
{part.content.split('\n').map((line, lineIndex) => {
const bulletMatch = line.match(/^(\s*)([-*])\s(.+)$/);
if (bulletMatch) {
const indent = bulletMatch[1].length;
return (
<div
key={lineIndex}
className="flex gap-2 mb-1"
style={{ paddingLeft: `${indent * 16}px` }}
>
<span className="text-vscode-foreground/60"></span>
<span className="flex-1">{bulletMatch[3]}</span>
</div>
);
}
const numberedMatch = line.match(/^(\s*)(\d+\.)\s(.+)$/);
if (numberedMatch) {
const indent = numberedMatch[1].length;
return (
<div
key={lineIndex}
className="flex gap-2 mb-1"
style={{ paddingLeft: `${indent * 16}px` }}
>
<span className="text-vscode-foreground/60 font-mono">
{numberedMatch[2]}
</span>
<span className="flex-1">{numberedMatch[3]}</span>
</div>
);
}
const headingMatch = line.match(/^(#{1,6})\s(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const headingLevel = Math.min(level + 2, 6);
const headingClassName =
'font-semibold text-vscode-foreground mb-2 mt-4 first:mt-0';
switch (headingLevel) {
case 3:
return (
<h3 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h3>
);
case 4:
return (
<h4 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h4>
);
case 5:
return (
<h5 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h5>
);
case 6:
return (
<h6 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h6>
);
default:
return (
<h3 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h3>
);
}
}
if (line.trim() === '') {
return <div key={lineIndex} className="h-2" />;
}
return (
<div key={lineIndex} className="mb-2 last:mb-0">
{line}
</div>
);
})}
</div>
);
})}
</div>
);
};
interface DetailsSectionProps {
title: string;
content?: string;
error?: string | null;
emptyMessage?: string;
defaultExpanded?: boolean;
}
export const DetailsSection: React.FC<DetailsSectionProps> = ({
title,
content,
error,
emptyMessage = 'No details available',
defaultExpanded = false
}) => {
return (
<CollapsibleSection title={title} defaultExpanded={defaultExpanded}>
<div className={title.toLowerCase().replace(/\s+/g, '-') + '-content'}>
{error ? (
<div className="text-sm text-red-400 py-2">
Error loading {title.toLowerCase()}: {error}
</div>
) : content !== undefined && content !== '' ? (
<MarkdownRenderer content={content} />
) : (
<div className="text-sm text-vscode-foreground/50 py-2">
{emptyMessage}
</div>
)}
</div>
</CollapsibleSection>
);
};

View File

@@ -0,0 +1,47 @@
import type React from 'react';
import type { TaskMasterTask } from '../../webview/types';
// Custom Priority Badge Component with theme-adaptive styling
export const PriorityBadge: React.FC<{
priority: TaskMasterTask['priority'];
}> = ({ priority }) => {
const getPriorityColors = (priority: string) => {
switch (priority) {
case 'high':
return {
backgroundColor: 'rgba(239, 68, 68, 0.2)', // red-500 with opacity
color: '#dc2626', // red-600 - works in both themes
borderColor: 'rgba(239, 68, 68, 0.4)'
};
case 'medium':
return {
backgroundColor: 'rgba(245, 158, 11, 0.2)', // amber-500 with opacity
color: '#d97706', // amber-600 - works in both themes
borderColor: 'rgba(245, 158, 11, 0.4)'
};
case 'low':
return {
backgroundColor: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity
color: '#16a34a', // green-600 - works in both themes
borderColor: 'rgba(34, 197, 94, 0.4)'
};
default:
return {
backgroundColor: 'rgba(156, 163, 175, 0.2)',
color: 'var(--vscode-foreground)',
borderColor: 'rgba(156, 163, 175, 0.4)'
};
}
};
const colors = getPriorityColors(priority || '');
return (
<span
className="inline-flex items-center px-2 py-1 text-xs font-medium rounded-md border"
style={colors}
>
{priority || 'None'}
</span>
);
};

View File

@@ -0,0 +1,218 @@
import type React from 'react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
import { Plus, Loader2 } from 'lucide-react';
import type { TaskMasterTask } from '../../webview/types';
import { getStatusDotColor } from '../constants';
interface SubtasksSectionProps {
currentTask: TaskMasterTask;
isSubtask: boolean;
sendMessage: (message: any) => Promise<any>;
onNavigateToTask: (taskId: string) => void;
}
export const SubtasksSection: React.FC<SubtasksSectionProps> = ({
currentTask,
isSubtask,
sendMessage,
onNavigateToTask
}) => {
const [isAddingSubtask, setIsAddingSubtask] = useState(false);
const [newSubtaskTitle, setNewSubtaskTitle] = useState('');
const [newSubtaskDescription, setNewSubtaskDescription] = useState('');
const [isSubmittingSubtask, setIsSubmittingSubtask] = useState(false);
const handleAddSubtask = async () => {
if (!currentTask || !newSubtaskTitle.trim() || isSubtask) {
return;
}
setIsSubmittingSubtask(true);
try {
await sendMessage({
type: 'addSubtask',
data: {
parentTaskId: currentTask.id,
subtaskData: {
title: newSubtaskTitle.trim(),
description: newSubtaskDescription.trim() || undefined,
status: 'pending'
}
}
});
// Reset form and close
setNewSubtaskTitle('');
setNewSubtaskDescription('');
setIsAddingSubtask(false);
} catch (error) {
console.error('❌ TaskDetailsView: Failed to add subtask:', error);
} finally {
setIsSubmittingSubtask(false);
}
};
const handleCancelAddSubtask = () => {
setIsAddingSubtask(false);
setNewSubtaskTitle('');
setNewSubtaskDescription('');
};
if (
!((currentTask.subtasks && currentTask.subtasks.length > 0) || !isSubtask)
) {
return null;
}
const rightElement = (
<>
{currentTask.subtasks && currentTask.subtasks.length > 0 && (
<span className="text-sm text-vscode-foreground/50">
{currentTask.subtasks?.filter((st) => st.status === 'done').length}/
{currentTask.subtasks?.length}
</span>
)}
{!isSubtask && (
<Button
variant="ghost"
size="sm"
className="ml-auto p-1 h-6 w-6 hover:bg-vscode-button-hoverBackground"
onClick={() => setIsAddingSubtask(true)}
title="Add subtask"
>
<Plus className="w-4 h-4" />
</Button>
)}
</>
);
return (
<CollapsibleSection
title="Sub-issues"
defaultExpanded={true}
rightElement={rightElement}
>
<div className="space-y-3">
{/* Add Subtask Form */}
{isAddingSubtask && (
<div className="bg-widget-background rounded-lg p-4 border border-widget-border">
<h4 className="text-sm font-medium text-vscode-foreground mb-3">
Add New Subtask
</h4>
<div className="space-y-3">
<div>
<Label
htmlFor="subtask-title"
className="block text-sm text-vscode-foreground/80 mb-1"
>
Title*
</Label>
<input
id="subtask-title"
type="text"
placeholder="Enter subtask title..."
value={newSubtaskTitle}
onChange={(e) => setNewSubtaskTitle(e.target.value)}
className="w-full px-3 py-2 text-sm bg-vscode-input-background border border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 rounded focus:border-vscode-focusBorder focus:ring-1 focus:ring-vscode-focusBorder"
disabled={isSubmittingSubtask}
/>
</div>
<div>
<Label
htmlFor="subtask-description"
className="block text-sm text-vscode-foreground/80 mb-1"
>
Description (Optional)
</Label>
<Textarea
id="subtask-description"
placeholder="Enter subtask description..."
value={newSubtaskDescription}
onChange={(e) => setNewSubtaskDescription(e.target.value)}
className="min-h-[80px] bg-vscode-input-background border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
disabled={isSubmittingSubtask}
/>
</div>
<div className="flex gap-3 pt-2">
<Button
onClick={handleAddSubtask}
disabled={!newSubtaskTitle.trim() || isSubmittingSubtask}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isSubmittingSubtask ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Adding...
</>
) : (
<>
<Plus className="w-4 h-4 mr-2" />
Add Subtask
</>
)}
</Button>
<Button
onClick={handleCancelAddSubtask}
variant="outline"
disabled={isSubmittingSubtask}
className="border-widget-border"
>
Cancel
</Button>
</div>
</div>
</div>
)}
{/* Subtasks List */}
{currentTask.subtasks && currentTask.subtasks.length > 0 && (
<div className="space-y-2">
{currentTask.subtasks.map((subtask, index) => {
const subtaskId = `${currentTask.id}.${index + 1}`;
return (
<div
key={subtask.id}
className="flex items-center gap-3 p-3 rounded-md border border-textSeparator-foreground hover:border-vscode-border/70 transition-colors cursor-pointer"
onClick={() => onNavigateToTask(subtaskId)}
>
<div
className="w-4 h-4 rounded-full flex items-center justify-center"
style={{
backgroundColor: getStatusDotColor(subtask.status)
}}
/>
<div className="flex-1 min-w-0">
<p className="text-sm text-vscode-foreground truncate">
{subtask.title}
</p>
{subtask.description && (
<p className="text-xs text-vscode-foreground/60 truncate mt-0.5">
{subtask.description}
</p>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Badge
variant="secondary"
className="text-xs bg-secondary/20 border-secondary/30 text-secondary-foreground px-2 py-0.5"
>
{subtask.status === 'pending' ? 'todo' : subtask.status}
</Badge>
</div>
</div>
);
})}
</div>
)}
</div>
</CollapsibleSection>
);
};

View File

@@ -0,0 +1,291 @@
import type React from 'react';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
import { PriorityBadge } from './PriorityBadge';
import type { TaskMasterTask } from '../../webview/types';
interface TaskMetadataSidebarProps {
currentTask: TaskMasterTask;
tasks: TaskMasterTask[];
complexity: any;
isSubtask: boolean;
sendMessage: (message: any) => Promise<any>;
onStatusChange: (status: TaskMasterTask['status']) => void;
onDependencyClick: (depId: string) => void;
isRegenerating?: boolean;
isAppending?: boolean;
}
export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
currentTask,
tasks,
complexity,
isSubtask,
sendMessage,
onStatusChange,
onDependencyClick,
isRegenerating = false,
isAppending = false
}) => {
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
const [mcpComplexityScore, setMcpComplexityScore] = useState<
number | undefined
>(undefined);
// Get complexity score from task
const currentComplexityScore = complexity?.score;
// Display logic - use MCP score if available, otherwise use current score
const displayComplexityScore =
mcpComplexityScore !== undefined
? mcpComplexityScore
: currentComplexityScore;
// Fetch complexity from MCP when needed
const fetchComplexityFromMCP = async (force = false) => {
if (!currentTask || (!force && currentComplexityScore !== undefined)) {
return;
}
setIsLoadingComplexity(true);
try {
const complexityResult = await sendMessage({
type: 'mcpRequest',
tool: 'complexity_report',
params: {}
});
if (complexityResult?.data?.report?.complexityAnalysis) {
const taskComplexity =
complexityResult.data.report.complexityAnalysis.tasks?.find(
(t: any) => t.id === currentTask.id
);
if (taskComplexity) {
setMcpComplexityScore(taskComplexity.complexityScore);
}
}
} catch (error) {
console.error('Failed to fetch complexity from MCP:', error);
} finally {
setIsLoadingComplexity(false);
}
};
// Handle running complexity analysis for a task
const handleRunComplexityAnalysis = async () => {
if (!currentTask) {
return;
}
setIsLoadingComplexity(true);
try {
// Run complexity analysis on this specific task
await sendMessage({
type: 'mcpRequest',
tool: 'analyze_project_complexity',
params: {
ids: currentTask.id.toString(),
research: false
}
});
// After analysis, fetch the updated complexity report
setTimeout(() => {
fetchComplexityFromMCP(true);
}, 1000);
} catch (error) {
console.error('Failed to run complexity analysis:', error);
} finally {
setIsLoadingComplexity(false);
}
};
// Effect to handle complexity on task change
useEffect(() => {
if (currentTask?.id) {
setMcpComplexityScore(undefined);
if (currentComplexityScore === undefined) {
fetchComplexityFromMCP();
}
}
}, [currentTask?.id, currentComplexityScore]);
return (
<div className="md:col-span-1 border-l border-textSeparator-foreground">
<div className="p-6">
<div className="space-y-6">
<div>
<h3 className="text-sm font-medium text-vscode-foreground/70 mb-3">
Properties
</h3>
</div>
<div className="space-y-4">
{/* Status */}
<div className="flex items-center justify-between">
<span className="text-sm text-vscode-foreground/70">Status</span>
<select
value={currentTask.status}
onChange={(e) =>
onStatusChange(e.target.value as TaskMasterTask['status'])
}
className="border rounded-md px-3 py-1 text-sm font-medium focus:ring-1 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
style={{
backgroundColor:
currentTask.status === 'pending'
? 'rgba(156, 163, 175, 0.2)'
: currentTask.status === 'in-progress'
? 'rgba(245, 158, 11, 0.2)'
: currentTask.status === 'review'
? 'rgba(59, 130, 246, 0.2)'
: currentTask.status === 'done'
? 'rgba(34, 197, 94, 0.2)'
: currentTask.status === 'deferred'
? 'rgba(239, 68, 68, 0.2)'
: 'var(--vscode-input-background)',
color:
currentTask.status === 'pending'
? 'var(--vscode-foreground)'
: currentTask.status === 'in-progress'
? '#d97706'
: currentTask.status === 'review'
? '#2563eb'
: currentTask.status === 'done'
? '#16a34a'
: currentTask.status === 'deferred'
? '#dc2626'
: 'var(--vscode-foreground)',
borderColor:
currentTask.status === 'pending'
? 'rgba(156, 163, 175, 0.4)'
: currentTask.status === 'in-progress'
? 'rgba(245, 158, 11, 0.4)'
: currentTask.status === 'review'
? 'rgba(59, 130, 246, 0.4)'
: currentTask.status === 'done'
? 'rgba(34, 197, 94, 0.4)'
: currentTask.status === 'deferred'
? 'rgba(239, 68, 68, 0.4)'
: 'var(--vscode-input-border)'
}}
>
<option value="pending">To do</option>
<option value="in-progress">In Progress</option>
<option value="review">Review</option>
<option value="done">Done</option>
<option value="deferred">Deferred</option>
</select>
</div>
{/* Priority */}
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Priority</span>
<PriorityBadge priority={currentTask.priority} />
</div>
{/* Complexity Score */}
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--vscode-foreground)]">
Complexity Score
</label>
{isLoadingComplexity ? (
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-[var(--vscode-descriptionForeground)]" />
<span className="text-sm text-[var(--vscode-descriptionForeground)]">
Loading...
</span>
</div>
) : displayComplexityScore !== undefined ? (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-[var(--vscode-foreground)]">
{displayComplexityScore}/10
</span>
<div
className={`flex-1 rounded-full h-2 ${
displayComplexityScore >= 7
? 'bg-red-500/20'
: displayComplexityScore >= 4
? 'bg-yellow-500/20'
: 'bg-green-500/20'
}`}
>
<div
className={`h-2 rounded-full transition-all duration-300 ${
displayComplexityScore >= 7
? 'bg-red-500'
: displayComplexityScore >= 4
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{
width: `${(displayComplexityScore || 0) * 10}%`
}}
/>
</div>
</div>
) : currentTask?.status === 'done' ||
currentTask?.status === 'deferred' ||
currentTask?.status === 'review' ? (
<div className="text-sm text-[var(--vscode-descriptionForeground)]">
N/A
</div>
) : (
<>
<div className="text-sm text-[var(--vscode-descriptionForeground)]">
No complexity score available
</div>
<div className="mt-3">
<Button
onClick={() => handleRunComplexityAnalysis()}
variant="outline"
size="sm"
className="text-xs"
disabled={isRegenerating || isAppending}
>
Run Complexity Analysis
</Button>
</div>
</>
)}
</div>
</div>
<div className="border-b border-textSeparator-foreground" />
{/* Dependencies */}
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
<div>
<h4 className="text-sm font-medium text-vscode-foreground/70 mb-3">
Dependencies
</h4>
<div className="space-y-2">
{currentTask.dependencies.map((depId) => {
// Convert both to string for comparison since depId might be string or number
const depTask = tasks.find(
(t) => String(t.id) === String(depId)
);
const fullTitle = `Task ${depId}: ${depTask?.title || 'Unknown Task'}`;
const truncatedTitle =
fullTitle.length > 40
? fullTitle.substring(0, 37) + '...'
: fullTitle;
return (
<div
key={depId}
className="text-sm text-link cursor-pointer hover:text-link-hover"
onClick={() => onDependencyClick(depId)}
title={fullTitle}
>
{truncatedTitle}
</div>
);
})}
</div>
</div>
)}
{/* Divider after Dependencies */}
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
<div className="border-b border-textSeparator-foreground" />
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,116 @@
import { useMemo } from 'react';
import { useTaskDetails as useTaskDetailsQuery } from '../../webview/hooks/useTaskQueries';
import type { TaskMasterTask } from '../../webview/types';
interface TaskFileData {
details?: string;
testStrategy?: string;
}
interface UseTaskDetailsProps {
taskId: string;
sendMessage: (message: any) => Promise<any>;
tasks: TaskMasterTask[];
}
export const useTaskDetails = ({
taskId,
sendMessage,
tasks
}: UseTaskDetailsProps) => {
// Parse task ID to determine if it's a subtask (e.g., "13.2")
const { isSubtask, parentId, subtaskIndex, taskIdForFetch } = useMemo(() => {
// Ensure taskId is a string
const taskIdStr = String(taskId);
const parts = taskIdStr.split('.');
if (parts.length === 2) {
return {
isSubtask: true,
parentId: parts[0],
subtaskIndex: parseInt(parts[1]) - 1, // Convert to 0-based index
taskIdForFetch: parts[0] // Always fetch parent task for subtasks
};
}
return {
isSubtask: false,
parentId: taskIdStr,
subtaskIndex: -1,
taskIdForFetch: taskIdStr
};
}, [taskId]);
// Use React Query to fetch full task details
const { data: fullTaskData, error: taskDetailsError } =
useTaskDetailsQuery(taskIdForFetch);
// Find current task from local state for immediate display
const { currentTask, parentTask } = useMemo(() => {
if (isSubtask) {
const parent = tasks.find((t) => t.id === parentId);
if (parent && parent.subtasks && parent.subtasks[subtaskIndex]) {
const subtask = parent.subtasks[subtaskIndex];
return { currentTask: subtask, parentTask: parent };
}
} else {
const task = tasks.find((t) => t.id === String(taskId));
if (task) {
return { currentTask: task, parentTask: null };
}
}
return { currentTask: null, parentTask: null };
}, [taskId, tasks, isSubtask, parentId, subtaskIndex]);
// Merge full task data from React Query with local state
const mergedCurrentTask = useMemo(() => {
if (!currentTask || !fullTaskData) return currentTask;
if (isSubtask && fullTaskData.subtasks) {
// Find the specific subtask in the full data
const subtaskData = fullTaskData.subtasks.find(
(st: any) =>
st.id === currentTask.id || st.id === parseInt(currentTask.id as any)
);
if (subtaskData) {
return { ...currentTask, ...subtaskData };
}
} else if (!isSubtask) {
// Merge parent task data
return { ...currentTask, ...fullTaskData };
}
return currentTask;
}, [currentTask, fullTaskData, isSubtask]);
// Extract task file data
const taskFileData: TaskFileData = useMemo(() => {
if (!mergedCurrentTask) return {};
return {
details: mergedCurrentTask.details || '',
testStrategy: mergedCurrentTask.testStrategy || ''
};
}, [mergedCurrentTask]);
// Get complexity score
const complexity = useMemo(() => {
if (mergedCurrentTask?.complexityScore !== undefined) {
return { score: mergedCurrentTask.complexityScore };
}
return null;
}, [mergedCurrentTask]);
// Function to refresh data after AI operations
const refreshComplexityAfterAI = () => {
// React Query will automatically refetch when mutations invalidate the query
// No need for manual refresh
};
return {
currentTask: mergedCurrentTask,
parentTask,
isSubtask,
taskFileData,
taskFileDataError: taskDetailsError ? 'Failed to load task details' : null,
complexity,
refreshComplexityAfterAI
};
};

View File

@@ -0,0 +1,218 @@
import type React from 'react';
import { useContext, useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { useQueryClient } from '@tanstack/react-query';
import { RefreshCw } from 'lucide-react';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator
} from '@/components/ui/breadcrumb';
import { VSCodeContext } from '../webview/contexts/VSCodeContext';
import { AIActionsSection } from './TaskDetails/AIActionsSection';
import { SubtasksSection } from './TaskDetails/SubtasksSection';
import { TaskMetadataSidebar } from './TaskDetails/TaskMetadataSidebar';
import { DetailsSection } from './TaskDetails/DetailsSection';
import { useTaskDetails } from './TaskDetails/useTaskDetails';
import { useTasks, taskKeys } from '../webview/hooks/useTaskQueries';
import type { TaskMasterTask } from '../webview/types';
interface TaskDetailsViewProps {
taskId: string;
onNavigateBack: () => void;
onNavigateToTask: (taskId: string) => void;
}
export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
taskId,
onNavigateBack,
onNavigateToTask
}) => {
const context = useContext(VSCodeContext);
if (!context) {
throw new Error('TaskDetailsView must be used within VSCodeProvider');
}
const { state, sendMessage } = context;
const { currentTag } = state;
const queryClient = useQueryClient();
const [isRefreshing, setIsRefreshing] = useState(false);
// Use React Query to fetch all tasks
const { data: allTasks = [] } = useTasks({ tag: currentTag });
const {
currentTask,
parentTask,
isSubtask,
taskFileData,
taskFileDataError,
complexity,
refreshComplexityAfterAI
} = useTaskDetails({ taskId, sendMessage, tasks: allTasks });
const handleStatusChange = async (newStatus: TaskMasterTask['status']) => {
if (!currentTask) return;
try {
await sendMessage({
type: 'updateTaskStatus',
data: {
taskId:
isSubtask && parentTask
? `${parentTask.id}.${currentTask.id}`
: currentTask.id,
newStatus: newStatus
}
});
} catch (error) {
console.error('❌ TaskDetailsView: Failed to update task status:', error);
}
};
const handleDependencyClick = (depId: string) => {
onNavigateToTask(depId);
};
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
try {
// Invalidate all task queries
await queryClient.invalidateQueries({ queryKey: taskKeys.all });
} finally {
// Reset after a short delay to show the animation
setTimeout(() => setIsRefreshing(false), 500);
}
}, [queryClient]);
if (!currentTask) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-lg text-vscode-foreground/70 mb-4">
Task not found
</p>
<Button onClick={onNavigateBack} variant="outline">
Back to Kanban Board
</Button>
</div>
</div>
);
}
return (
<div className="h-full flex flex-col">
<div className="flex-1 grid grid-cols-1 md:grid-cols-3 gap-6 p-6 overflow-auto">
{/* Left column - Main content (2/3 width) */}
<div className="md:col-span-2 space-y-6">
{/* Breadcrumb navigation */}
<div className="flex items-center justify-between">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
onClick={onNavigateBack}
className="cursor-pointer hover:text-vscode-foreground text-link"
>
Kanban Board
</BreadcrumbLink>
</BreadcrumbItem>
{isSubtask && parentTask && (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink
onClick={() => onNavigateToTask(parentTask.id)}
className="cursor-pointer hover:text-vscode-foreground"
>
{parentTask.title}
</BreadcrumbLink>
</BreadcrumbItem>
</>
)}
<BreadcrumbSeparator />
<BreadcrumbItem>
<span className="text-vscode-foreground">
{currentTask.title}
</span>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<button
onClick={handleRefresh}
disabled={isRefreshing}
className="p-1.5 rounded hover:bg-vscode-button-hoverBackground transition-colors"
title="Refresh task details"
>
<RefreshCw
className={`w-4 h-4 text-vscode-foreground/70 ${isRefreshing ? 'animate-spin' : ''}`}
/>
</button>
</div>
{/* Task title */}
<h1 className="text-2xl font-bold tracking-tight text-vscode-foreground">
{currentTask.title}
</h1>
{/* Description */}
<div className="mb-8">
<p className="text-vscode-foreground/80 leading-relaxed">
{currentTask.description || 'No description available.'}
</p>
</div>
{/* AI Actions */}
<AIActionsSection
currentTask={currentTask}
isSubtask={isSubtask}
parentTask={parentTask}
sendMessage={sendMessage}
refreshComplexityAfterAI={refreshComplexityAfterAI}
/>
{/* Implementation Details */}
<DetailsSection
title="Implementation Details"
content={taskFileData.details}
error={taskFileDataError}
emptyMessage="No implementation details available"
defaultExpanded={false}
/>
{/* Test Strategy */}
<DetailsSection
title="Test Strategy"
content={taskFileData.testStrategy}
error={taskFileDataError}
emptyMessage="No test strategy available"
defaultExpanded={false}
/>
{/* Subtasks */}
<SubtasksSection
currentTask={currentTask}
isSubtask={isSubtask}
sendMessage={sendMessage}
onNavigateToTask={onNavigateToTask}
/>
</div>
{/* Right column - Metadata (1/3 width) */}
<TaskMetadataSidebar
currentTask={currentTask}
tasks={allTasks}
complexity={complexity}
isSubtask={isSubtask}
sendMessage={sendMessage}
onStatusChange={handleStatusChange}
onDependencyClick={handleDependencyClick}
/>
</div>
</div>
);
};
export default TaskDetailsView;

View File

@@ -0,0 +1,23 @@
import React from 'react';
interface TaskMasterLogoProps {
className?: string;
}
export const TaskMasterLogo: React.FC<TaskMasterLogoProps> = ({
className = ''
}) => {
return (
<svg
className={className}
viewBox="0 0 224 291"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M101.635 286.568L71.4839 256.414C65.6092 250.539 65.6092 241.03 71.4839 235.155L142.52 164.11C144.474 162.156 147.643 162.156 149.61 164.11L176.216 190.719C178.17 192.673 181.339 192.673 183.305 190.719L189.719 184.305C191.673 182.35 191.673 179.181 189.719 177.214L163.113 150.605C161.159 148.651 161.159 145.481 163.113 143.514L191.26 115.365C193.214 113.41 193.214 110.241 191.26 108.274L182.316 99.3291C180.362 97.3748 177.193 97.3748 175.226 99.3291L55.8638 218.706C49.989 224.581 40.4816 224.581 34.6068 218.706L4.4061 188.501C-1.4687 182.626 -1.4687 173.117 4.4061 167.242L23.8342 147.811C25.7883 145.857 25.7883 142.688 23.8342 140.721L4.78187 121.666C-1.09293 115.791 -1.09293 106.282 4.78187 100.406L34.7195 70.4527C40.5943 64.5772 50.1017 64.5772 55.9765 70.4527L75.555 90.0335C77.5091 91.9879 80.6782 91.9879 82.6448 90.0335L124.144 48.5292C126.098 46.5749 126.098 43.4054 124.144 41.4385L115.463 32.7568C113.509 30.8025 110.34 30.8025 108.374 32.7568L99.8683 41.2632C97.9143 43.2175 94.7451 43.2175 92.7785 41.2632L82.1438 30.6271C80.1897 28.6728 80.1897 25.5033 82.1438 23.5364L101.271 4.40662C107.146 -1.46887 116.653 -1.46887 122.528 4.40662L152.478 34.3604C158.353 40.2359 158.353 49.7444 152.478 55.6199L82.6323 125.474C80.6782 127.429 77.5091 127.429 75.5425 125.474L48.8741 98.8029C46.9201 96.8486 43.7509 96.8486 41.7843 98.8029L33.1036 107.485C31.1496 109.439 31.1496 112.608 33.1036 114.575L59.2458 140.721C61.1999 142.675 61.1999 145.844 59.2458 147.811L32.7404 174.32C30.7863 176.274 30.7863 179.444 32.7404 181.411L41.6841 190.355C43.6382 192.31 46.8073 192.31 48.7739 190.355L168.136 70.9789C174.011 65.1034 183.518 65.1034 189.393 70.9789L219.594 101.183C225.469 107.059 225.469 116.567 219.594 122.443L198.537 143.502C196.583 145.456 196.583 148.626 198.537 150.592L218.053 170.111C223.928 175.986 223.928 185.495 218.053 191.37L190.37 219.056C184.495 224.932 174.988 224.932 169.113 219.056L149.597 199.538C147.643 197.584 144.474 197.584 142.508 199.538L99.8057 242.245C97.8516 244.2 97.8516 247.369 99.8057 249.336L108.699 258.231C110.653 260.185 113.823 260.185 115.789 258.231L122.954 251.065C124.908 249.11 128.077 249.11 130.044 251.065L140.679 261.701C142.633 263.655 142.633 266.825 140.679 268.791L122.879 286.593C117.004 292.469 107.497 292.469 101.622 286.593L101.635 286.568Z"
fill="currentColor"
/>
</svg>
);
};

View File

@@ -0,0 +1,26 @@
/**
* Shared constants for TaskDetails components
*/
/**
* Status color definitions for visual indicators
*/
export const STATUS_DOT_COLORS = {
done: '#22c55e', // Green
'in-progress': '#3b82f6', // Blue
review: '#a855f7', // Purple
deferred: '#ef4444', // Red
cancelled: '#6b7280', // Gray
pending: '#eab308' // Yellow (default)
} as const;
export type TaskStatus = keyof typeof STATUS_DOT_COLORS;
/**
* Get the color for a status dot indicator
* @param status - The task status
* @returns The hex color code for the status
*/
export function getStatusDotColor(status: string): string {
return STATUS_DOT_COLORS[status as TaskStatus] || STATUS_DOT_COLORS.pending;
}

View File

@@ -0,0 +1,61 @@
import type React from 'react';
import { useState } from 'react';
import { Button } from './button';
import { ChevronDown, ChevronRight } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
interface CollapsibleSectionProps {
title: string;
icon?: LucideIcon;
defaultExpanded?: boolean;
className?: string;
headerClassName?: string;
contentClassName?: string;
buttonClassName?: string;
children: React.ReactNode;
rightElement?: React.ReactNode;
}
export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
title,
icon: Icon,
defaultExpanded = false,
className = '',
headerClassName = '',
contentClassName = '',
buttonClassName = 'text-vscode-foreground/70 hover:text-vscode-foreground',
children,
rightElement
}) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
return (
<div className={`mb-8 ${className}`}>
<div className={`flex items-center gap-2 mb-4 ${headerClassName}`}>
<Button
variant="ghost"
size="sm"
className={`p-0 h-auto ${buttonClassName}`}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<ChevronDown className="w-4 h-4 mr-1" />
) : (
<ChevronRight className="w-4 h-4 mr-1" />
)}
{Icon && <Icon className="w-4 h-4 mr-1" />}
{title}
</Button>
{rightElement}
</div>
{isExpanded && (
<div
className={`bg-widget-background rounded-lg p-4 border border-widget-border ${contentClassName}`}
>
{children}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,46 @@
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps, cva } from 'class-variance-authority';
import type * as React from 'react';
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'
}
}
);
function Badge({
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}
/>
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,109 @@
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';
import type * as React from 'react';
import { cn } from '@/lib/utils';
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 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;
}) {
const Comp = asChild ? Slot : 'a';
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 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>
);
}
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>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis
};

View File

@@ -0,0 +1,59 @@
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps, cva } from 'class-variance-authority';
import type * as React from 'react';
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'
}
}
);
function 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}
/>
);
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,92 @@
import type * as React from 'react';
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 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 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 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}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent
};

View File

@@ -0,0 +1,31 @@
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
);
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -0,0 +1,257 @@
'use client';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import type * as React from 'react';
import { cn } from '@/lib/utils';
const DROPDOWN_MENU_ITEM_CLASSES =
"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";
const DROPDOWN_MENU_SUB_CONTENT_CLASSES =
'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';
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
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>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: 'default' | 'destructive';
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(DROPDOWN_MENU_ITEM_CLASSES, className)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
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>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
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>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
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}
/>
);
}
function DropdownMenuSeparator({
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}
/>
);
}
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}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
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>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(DROPDOWN_MENU_SUB_CONTENT_CLASSES, className)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent
};

View File

@@ -0,0 +1,22 @@
import * as LabelPrimitive from '@radix-ui/react-label';
import type * as React from 'react';
import { cn } from '@/lib/utils';
function Label({
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}
/>
);
}
export { Label };

View File

@@ -0,0 +1,56 @@
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import type * as React from 'react';
import { cn } from '@/lib/utils';
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn('relative overflow-hidden', 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 overflow-y-auto"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
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>
);
}
export { ScrollArea, ScrollBar };

View File

@@ -0,0 +1,28 @@
'use client';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import type * as React from 'react';
import { cn } from '@/lib/utils';
function Separator({
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}
/>
);
}
export { Separator };

View File

@@ -0,0 +1,188 @@
'use client';
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import {
DndContext,
DragOverlay,
MouseSensor,
TouchSensor,
rectIntersection,
useDraggable,
useDroppable,
useSensor,
useSensors
} from '@dnd-kit/core';
import type { DragEndEvent } from '@dnd-kit/core';
import type React from 'react';
import type { ReactNode } from 'react';
export type { DragEndEvent } from '@dnd-kit/core';
export type Status = {
id: string;
name: string;
color: string;
};
export type Feature = {
id: string;
name: string;
startAt: Date;
endAt: Date;
status: Status;
};
export type KanbanBoardProps = {
id: Status['id'];
children: ReactNode;
className?: string;
};
export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => {
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>
);
};
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;
};
export const KanbanCard = ({
id,
name,
index,
parent,
children,
className,
onClick,
onDoubleClick
}: KanbanCardProps) => {
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>
);
};
export type KanbanCardsProps = {
children: ReactNode;
className?: string;
};
export const KanbanCards = ({ children, className }: KanbanCardsProps) => (
<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;
};
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>
);
export type KanbanProviderProps = {
children: ReactNode;
onDragEnd: (event: DragEndEvent) => void;
onDragStart?: (event: DragEndEvent) => void;
onDragCancel?: () => void;
className?: string;
dragOverlay?: ReactNode;
};
export const KanbanProvider = ({
children,
onDragEnd,
onDragStart,
onDragCancel,
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 }
})
);
return (
<DndContext
sensors={sensors}
collisionDetection={rectIntersection}
onDragEnd={onDragEnd}
onDragStart={onDragStart}
onDragCancel={onDragCancel}
>
<div
className={cn(
'grid w-full auto-cols-fr grid-flow-col gap-4',
className
)}
>
{children}
</div>
<DragOverlay>{dragOverlay}</DragOverlay>
</DndContext>
);
};

View File

@@ -0,0 +1,18 @@
import type * as React from 'react';
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}
/>
);
}
export { Textarea };

View File

@@ -0,0 +1,219 @@
/**
* TaskMaster Extension - Simplified Architecture
* Only using patterns where they add real value
*/
import * as vscode from 'vscode';
import { ConfigService } from './services/config-service';
import { PollingService } from './services/polling-service';
import { createPollingStrategy } from './services/polling-strategies';
import { TaskRepository } from './services/task-repository';
import { WebviewManager } from './services/webview-manager';
import { EventEmitter } from './utils/event-emitter';
import { ExtensionLogger } from './utils/logger';
import {
MCPClientManager,
createMCPConfigFromSettings
} from './utils/mcpClient';
import { TaskMasterApi } from './utils/task-master-api';
import { SidebarWebviewManager } from './services/sidebar-webview-manager';
let logger: ExtensionLogger;
let mcpClient: MCPClientManager;
let api: TaskMasterApi;
let repository: TaskRepository;
let pollingService: PollingService;
let webviewManager: WebviewManager;
let events: EventEmitter;
let configService: ConfigService;
let sidebarManager: SidebarWebviewManager;
export async function activate(context: vscode.ExtensionContext) {
try {
// Initialize logger (needed to prevent MCP stdio issues)
logger = ExtensionLogger.getInstance();
logger.log('🎉 TaskMaster Extension activating...');
// Simple event emitter for webview communication
events = new EventEmitter();
// Initialize MCP client
mcpClient = new MCPClientManager(createMCPConfigFromSettings());
// Initialize API
api = new TaskMasterApi(mcpClient);
// Repository with caching (actually useful for performance)
repository = new TaskRepository(api, logger);
// Config service for TaskMaster config.json
configService = new ConfigService(logger);
// Polling service with strategy pattern (makes sense for different polling behaviors)
const strategy = createPollingStrategy(
vscode.workspace.getConfiguration('taskmaster')
);
pollingService = new PollingService(repository, strategy, logger);
// Webview manager (cleaner than global panel array) - create before connection
webviewManager = new WebviewManager(context, repository, events, logger);
webviewManager.setConfigService(configService);
// Sidebar webview manager
sidebarManager = new SidebarWebviewManager(context.extensionUri);
// Initialize connection
await initializeConnection();
// Set MCP client and API after connection
webviewManager.setMCPClient(mcpClient);
webviewManager.setApi(api);
sidebarManager.setApi(api);
// Register commands
registerCommands(context);
// Handle polling lifecycle
events.on('webview:opened', () => {
if (webviewManager.getPanelCount() === 1) {
pollingService.start();
}
});
events.on('webview:closed', () => {
if (webviewManager.getPanelCount() === 0) {
pollingService.stop();
}
});
// Forward repository updates to webviews
repository.on('tasks:updated', (tasks) => {
webviewManager.broadcast('tasksUpdated', { tasks, source: 'polling' });
});
logger.log('✅ TaskMaster Extension activated');
} catch (error) {
logger?.error('Failed to activate', error);
vscode.window.showErrorMessage(
`Failed to activate TaskMaster: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
async function initializeConnection() {
try {
logger.log('🔗 Connecting to TaskMaster...');
// Notify webviews that we're connecting
if (webviewManager) {
webviewManager.broadcast('connectionStatus', {
isConnected: false,
status: 'Connecting...'
});
}
await mcpClient.connect();
const testResult = await api.testConnection();
if (testResult.success) {
logger.log('✅ Connected to TaskMaster');
vscode.window.showInformationMessage('TaskMaster connected!');
// Notify webviews that we're connected
if (webviewManager) {
webviewManager.broadcast('connectionStatus', {
isConnected: true,
status: 'Connected'
});
}
if (sidebarManager) {
sidebarManager.updateConnectionStatus();
}
} else {
throw new Error(testResult.error || 'Connection test failed');
}
} catch (error) {
logger.error('Connection failed', error);
// Notify webviews that connection failed
if (webviewManager) {
webviewManager.broadcast('connectionStatus', {
isConnected: false,
status: 'Disconnected'
});
}
if (sidebarManager) {
sidebarManager.updateConnectionStatus();
}
handleConnectionError(error);
}
}
function handleConnectionError(error: any) {
const message = error instanceof Error ? error.message : 'Unknown error';
if (message.includes('ENOENT') && message.includes('npx')) {
vscode.window
.showWarningMessage(
'TaskMaster: npx not found. Please ensure Node.js is installed.',
'Open Settings'
)
.then((action) => {
if (action === 'Open Settings') {
vscode.commands.executeCommand(
'workbench.action.openSettings',
'@ext:Hamster.task-master-hamster taskmaster'
);
}
});
} else {
vscode.window.showWarningMessage(
`TaskMaster connection failed: ${message}`
);
}
}
function registerCommands(context: vscode.ExtensionContext) {
// Main command
context.subscriptions.push(
vscode.commands.registerCommand('tm.showKanbanBoard', async () => {
await webviewManager.createOrShowPanel();
})
);
// Utility commands
context.subscriptions.push(
vscode.commands.registerCommand('tm.refreshTasks', async () => {
await repository.refresh();
vscode.window.showInformationMessage('Tasks refreshed!');
})
);
context.subscriptions.push(
vscode.commands.registerCommand('tm.openSettings', () => {
vscode.commands.executeCommand(
'workbench.action.openSettings',
'@ext:Hamster.task-master-hamster taskmaster'
);
})
);
// Register sidebar view provider
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
'taskmaster.welcome',
sidebarManager
)
);
}
export function deactivate() {
logger?.log('👋 TaskMaster Extension deactivating...');
pollingService?.stop();
webviewManager?.dispose();
api?.destroy();
mcpClient?.disconnect();
}

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,147 @@
/**
* Config Service
* Manages Task Master config.json file operations
*/
import * as path from 'path';
import * as fs from 'fs/promises';
import * as vscode from 'vscode';
import type { ExtensionLogger } from '../utils/logger';
export interface TaskMasterConfigJson {
anthropicApiKey?: string;
perplexityApiKey?: string;
openaiApiKey?: string;
googleApiKey?: string;
xaiApiKey?: string;
openrouterApiKey?: string;
mistralApiKey?: string;
debug?: boolean;
models?: {
main?: string;
research?: string;
fallback?: string;
};
}
export class ConfigService {
private configCache: TaskMasterConfigJson | null = null;
private lastReadTime = 0;
private readonly CACHE_DURATION = 5000; // 5 seconds
constructor(private logger: ExtensionLogger) {}
/**
* Read Task Master config.json from the workspace
*/
async readConfig(): Promise<TaskMasterConfigJson | null> {
// Check cache first
if (
this.configCache &&
Date.now() - this.lastReadTime < this.CACHE_DURATION
) {
return this.configCache;
}
try {
const workspaceRoot = this.getWorkspaceRoot();
if (!workspaceRoot) {
this.logger.warn('No workspace folder found');
return null;
}
const configPath = path.join(workspaceRoot, '.taskmaster', 'config.json');
try {
const configContent = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(configContent) as TaskMasterConfigJson;
// Cache the result
this.configCache = config;
this.lastReadTime = Date.now();
this.logger.debug('Successfully read Task Master config', {
hasModels: !!config.models,
debug: config.debug
});
return config;
} catch (error) {
if ((error as any).code === 'ENOENT') {
this.logger.debug('Task Master config.json not found');
} else {
this.logger.error('Failed to read Task Master config', error);
}
return null;
}
} catch (error) {
this.logger.error('Error accessing Task Master config', error);
return null;
}
}
/**
* Get safe config for display (with sensitive data masked)
*/
async getSafeConfig(): Promise<Record<string, any> | null> {
const config = await this.readConfig();
if (!config) {
return null;
}
// Create a safe copy with masked API keys
const safeConfig: Record<string, any> = {
...config
};
// Mask all API keys
const apiKeyFields = [
'anthropicApiKey',
'perplexityApiKey',
'openaiApiKey',
'googleApiKey',
'xaiApiKey',
'openrouterApiKey',
'mistralApiKey'
];
for (const field of apiKeyFields) {
if (safeConfig[field]) {
safeConfig[field] = this.maskApiKey(safeConfig[field]);
}
}
return safeConfig;
}
/**
* Mask API key for display
* Shows only the last 4 characters for better security
*/
private maskApiKey(key: string): string {
if (key.length <= 4) {
return '****';
}
const visibleChars = 4;
const maskedLength = key.length - visibleChars;
return (
'*'.repeat(Math.min(maskedLength, 12)) +
key.substring(key.length - visibleChars)
);
}
/**
* Clear cache
*/
clearCache(): void {
this.configCache = null;
this.lastReadTime = 0;
}
/**
* Get workspace root path
*/
private getWorkspaceRoot(): string | undefined {
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
}
}

View File

@@ -0,0 +1,330 @@
/**
* Error Handler Service
* Centralized error handling with categorization and recovery strategies
*/
import * as vscode from 'vscode';
import type { ExtensionLogger } from '../utils/logger';
export enum ErrorSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export enum ErrorCategory {
MCP_CONNECTION = 'mcp_connection',
CONFIGURATION = 'configuration',
TASK_LOADING = 'task_loading',
NETWORK = 'network',
INTERNAL = 'internal'
}
export interface ErrorContext {
category: ErrorCategory;
severity: ErrorSeverity;
message: string;
originalError?: Error | unknown;
operation?: string;
taskId?: string;
isRecoverable?: boolean;
suggestedActions?: string[];
}
export class ErrorHandler {
private errorLog: Map<string, ErrorContext> = new Map();
private errorId = 0;
constructor(private logger: ExtensionLogger) {}
/**
* Handle an error with appropriate logging and user notification
*/
handleError(context: ErrorContext): string {
const errorId = `error_${++this.errorId}`;
this.errorLog.set(errorId, context);
// Log to extension logger
this.logError(context);
// Show user notification if appropriate
this.notifyUser(context);
return errorId;
}
/**
* Log error based on severity
*/
private logError(context: ErrorContext): void {
const logMessage = `[${context.category}] ${context.message}`;
const details = {
operation: context.operation,
taskId: context.taskId,
error: context.originalError
};
switch (context.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
this.logger.error(logMessage, details);
break;
case ErrorSeverity.MEDIUM:
this.logger.warn(logMessage, details);
break;
case ErrorSeverity.LOW:
this.logger.debug(logMessage, details);
break;
}
}
/**
* Show user notification based on severity and category
*/
/**
* Validate if an action is allowed
*/
private isValidAction(action: string): boolean {
// Define predefined valid actions
const predefinedActions = [
'Retry',
'Settings',
'Reload',
'Dismiss',
'View Logs',
'Report Issue'
];
// Check if it's a predefined action or a TaskMaster command
return predefinedActions.includes(action) || action.startsWith('tm.');
}
/**
* Filter and validate suggested actions
*/
private getValidActions(actions: string[]): string[] {
return actions.filter((action) => this.isValidAction(action));
}
private notifyUser(context: ErrorContext): void {
// Don't show low severity errors to users
if (context.severity === ErrorSeverity.LOW) {
return;
}
// Validate and filter suggested actions
const rawActions = context.suggestedActions || [];
const actions = this.getValidActions(rawActions);
// Log if any actions were filtered out
if (rawActions.length !== actions.length) {
this.logger.warn('Invalid actions filtered out:', {
original: rawActions,
filtered: actions,
removed: rawActions.filter((a) => !actions.includes(a))
});
}
switch (context.severity) {
case ErrorSeverity.CRITICAL:
vscode.window
.showErrorMessage(`TaskMaster: ${context.message}`, ...actions)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
break;
case ErrorSeverity.HIGH:
if (context.category === ErrorCategory.MCP_CONNECTION) {
// Use validated actions or default actions for MCP connection
const mcpActions =
actions.length > 0 ? actions : ['Retry', 'Settings'];
vscode.window
.showWarningMessage(`TaskMaster: ${context.message}`, ...mcpActions)
.then((action) => {
if (action === 'Retry') {
vscode.commands.executeCommand('tm.reconnect');
} else if (action === 'Settings') {
vscode.commands.executeCommand('tm.openSettings');
} else if (action) {
this.handleUserAction(action, context);
}
});
} else {
// Show warning with validated actions
if (actions.length > 0) {
vscode.window
.showWarningMessage(`TaskMaster: ${context.message}`, ...actions)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
} else {
vscode.window.showWarningMessage(`TaskMaster: ${context.message}`);
}
}
break;
case ErrorSeverity.MEDIUM:
// Only show medium errors for important categories
if (
[ErrorCategory.CONFIGURATION, ErrorCategory.TASK_LOADING].includes(
context.category
)
) {
if (actions.length > 0) {
vscode.window
.showInformationMessage(
`TaskMaster: ${context.message}`,
...actions
)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
} else {
vscode.window.showInformationMessage(
`TaskMaster: ${context.message}`
);
}
}
break;
}
}
/**
* Handle user action from notification
*/
private handleUserAction(action: string, context: ErrorContext): void {
this.logger.debug(`User selected action: ${action}`, {
errorContext: context
});
// Handle predefined actions
switch (action) {
case 'Retry':
if (context.category === ErrorCategory.MCP_CONNECTION) {
vscode.commands.executeCommand('tm.reconnect');
} else {
vscode.commands.executeCommand('tm.refreshTasks');
}
break;
case 'Settings':
vscode.commands.executeCommand('tm.openSettings');
break;
case 'Reload':
vscode.commands.executeCommand('workbench.action.reloadWindow');
break;
case 'View Logs':
// Show error details in a modal dialog instead of output channel
this.showErrorDetails(context);
break;
case 'Report Issue':
const issueUrl = this.generateIssueUrl(context);
vscode.env.openExternal(vscode.Uri.parse(issueUrl));
break;
case 'Dismiss':
// No action needed
break;
default:
// Handle TaskMaster commands (tm.*)
if (action.startsWith('tm.')) {
void vscode.commands.executeCommand(action).then(
() => {},
(error: unknown) => {
this.logger.error(`Failed to execute command: ${action}`, error);
}
);
}
break;
}
}
/**
* Show detailed error information in a modal dialog
*/
private showErrorDetails(context: ErrorContext): void {
const details = [
`**Error Details**`,
``,
`Category: ${context.category}`,
`Severity: ${context.severity}`,
`Message: ${context.message}`,
context.operation ? `Operation: ${context.operation}` : '',
context.taskId ? `Task ID: ${context.taskId}` : '',
context.originalError ? `\nOriginal Error:\n${context.originalError}` : ''
]
.filter(Boolean)
.join('\n');
vscode.window.showInformationMessage(details, {
modal: true,
detail: details
});
}
/**
* Generate GitHub issue URL with pre-filled information
*/
private generateIssueUrl(context: ErrorContext): string {
const title = encodeURIComponent(`[Extension Error] ${context.message}`);
const body = encodeURIComponent(
[
`**Error Details:**`,
`- Category: ${context.category}`,
`- Severity: ${context.severity}`,
`- Message: ${context.message}`,
context.operation ? `- Operation: ${context.operation}` : '',
context.taskId ? `- Task ID: ${context.taskId}` : '',
``,
`**Context:**`,
'```json',
JSON.stringify(context, null, 2),
'```',
``,
`**Environment:**`,
`- VS Code Version: ${vscode.version}`,
`- Extension Version: ${vscode.extensions.getExtension('Hamster.taskmaster')?.packageJSON.version || 'Unknown'}`,
``,
`**Steps to Reproduce:**`,
`1. [Please describe the steps that led to this error]`,
``,
`**Expected Behavior:**`,
`[What should have happened instead]`
]
.filter(Boolean)
.join('\n')
);
return `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${title}&body=${body}`;
}
/**
* Get error by ID
*/
getError(errorId: string): ErrorContext | undefined {
return this.errorLog.get(errorId);
}
/**
* Clear old errors (keep last 100)
*/
clearOldErrors(): void {
if (this.errorLog.size > 100) {
const entriesToKeep = Array.from(this.errorLog.entries()).slice(-100);
this.errorLog.clear();
entriesToKeep.forEach(([id, error]) => this.errorLog.set(id, error));
}
}
}

View File

@@ -0,0 +1,129 @@
/**
* Notification Preferences Service
* Manages user preferences for notifications
*/
import * as vscode from 'vscode';
import { ErrorCategory, ErrorSeverity } from './error-handler';
export enum NotificationLevel {
ALL = 'all',
ERRORS_ONLY = 'errors_only',
CRITICAL_ONLY = 'critical_only',
NONE = 'none'
}
interface NotificationRule {
category: ErrorCategory;
minSeverity: ErrorSeverity;
enabled: boolean;
}
export class NotificationPreferences {
private defaultRules: NotificationRule[] = [
{
category: ErrorCategory.MCP_CONNECTION,
minSeverity: ErrorSeverity.HIGH,
enabled: true
},
{
category: ErrorCategory.CONFIGURATION,
minSeverity: ErrorSeverity.MEDIUM,
enabled: true
},
{
category: ErrorCategory.TASK_LOADING,
minSeverity: ErrorSeverity.HIGH,
enabled: true
},
{
category: ErrorCategory.NETWORK,
minSeverity: ErrorSeverity.HIGH,
enabled: true
},
{
category: ErrorCategory.INTERNAL,
minSeverity: ErrorSeverity.CRITICAL,
enabled: true
}
];
/**
* Check if a notification should be shown
*/
shouldShowNotification(
category: ErrorCategory,
severity: ErrorSeverity
): boolean {
// Get user's notification level preference
const level = this.getNotificationLevel();
if (level === NotificationLevel.NONE) {
return false;
}
if (
level === NotificationLevel.CRITICAL_ONLY &&
severity !== ErrorSeverity.CRITICAL
) {
return false;
}
if (
level === NotificationLevel.ERRORS_ONLY &&
severity !== ErrorSeverity.CRITICAL &&
severity !== ErrorSeverity.HIGH
) {
return false;
}
// Check category-specific rules
const rule = this.defaultRules.find((r) => r.category === category);
if (!rule || !rule.enabled) {
return false;
}
// Check if severity meets minimum threshold
return this.compareSeverity(severity, rule.minSeverity) >= 0;
}
/**
* Get user's notification level preference
*/
private getNotificationLevel(): NotificationLevel {
const config = vscode.workspace.getConfiguration('taskmaster');
return config.get<NotificationLevel>(
'notifications.level',
NotificationLevel.ERRORS_ONLY
);
}
/**
* Compare severity levels
*/
private compareSeverity(a: ErrorSeverity, b: ErrorSeverity): number {
const severityOrder = {
[ErrorSeverity.LOW]: 0,
[ErrorSeverity.MEDIUM]: 1,
[ErrorSeverity.HIGH]: 2,
[ErrorSeverity.CRITICAL]: 3
};
return severityOrder[a] - severityOrder[b];
}
/**
* Get toast notification duration based on severity
*/
getToastDuration(severity: ErrorSeverity): number {
switch (severity) {
case ErrorSeverity.CRITICAL:
return 10000; // 10 seconds
case ErrorSeverity.HIGH:
return 7000; // 7 seconds
case ErrorSeverity.MEDIUM:
return 5000; // 5 seconds
case ErrorSeverity.LOW:
return 3000; // 3 seconds
}
}
}

View File

@@ -0,0 +1,92 @@
/**
* Polling Service - Simplified version
* Uses strategy pattern for different polling behaviors
*/
import type { ExtensionLogger } from '../utils/logger';
import type { TaskRepository } from './task-repository';
export interface PollingStrategy {
calculateNextInterval(
consecutiveNoChanges: number,
lastChangeTime?: number
): number;
getName(): string;
}
export class PollingService {
private timer?: NodeJS.Timeout;
private consecutiveNoChanges = 0;
private lastChangeTime?: number;
private lastTasksJson?: string;
constructor(
private repository: TaskRepository,
private strategy: PollingStrategy,
private logger: ExtensionLogger
) {}
start(): void {
if (this.timer) {
return;
}
this.logger.log(
`Starting polling with ${this.strategy.getName()} strategy`
);
this.scheduleNextPoll();
}
stop(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
this.logger.log('Polling stopped');
}
}
setStrategy(strategy: PollingStrategy): void {
this.strategy = strategy;
this.logger.log(`Changed to ${strategy.getName()} polling strategy`);
// Restart with new strategy if running
if (this.timer) {
this.stop();
this.start();
}
}
private async poll(): Promise<void> {
try {
const tasks = await this.repository.getAll();
const tasksJson = JSON.stringify(tasks);
// Check for changes
if (tasksJson !== this.lastTasksJson) {
this.consecutiveNoChanges = 0;
this.lastChangeTime = Date.now();
this.logger.debug('Tasks changed');
} else {
this.consecutiveNoChanges++;
}
this.lastTasksJson = tasksJson;
} catch (error) {
this.logger.error('Polling error', error);
}
}
private scheduleNextPoll(): void {
const interval = this.strategy.calculateNextInterval(
this.consecutiveNoChanges,
this.lastChangeTime
);
this.timer = setTimeout(async () => {
await this.poll();
this.scheduleNextPoll();
}, interval);
this.logger.debug(`Next poll in ${interval}ms`);
}
}

View File

@@ -0,0 +1,67 @@
/**
* Polling Strategies - Simplified
* Different algorithms for polling intervals
*/
import type { PollingStrategy } from './polling-service';
/**
* Fixed interval polling
*/
export class FixedIntervalStrategy implements PollingStrategy {
constructor(private interval = 10000) {}
calculateNextInterval(): number {
return this.interval;
}
getName(): string {
return 'fixed';
}
}
/**
* Adaptive polling based on activity
*/
export class AdaptivePollingStrategy implements PollingStrategy {
private readonly MIN_INTERVAL = 5000; // 5 seconds
private readonly MAX_INTERVAL = 60000; // 1 minute
private readonly BASE_INTERVAL = 10000; // 10 seconds
calculateNextInterval(consecutiveNoChanges: number): number {
// Start with base interval
let interval = this.BASE_INTERVAL;
// If no changes for a while, slow down
if (consecutiveNoChanges > 5) {
interval = Math.min(
this.MAX_INTERVAL,
this.BASE_INTERVAL * 1.5 ** (consecutiveNoChanges - 5)
);
} else if (consecutiveNoChanges === 0) {
// Recent change, poll more frequently
interval = this.MIN_INTERVAL;
}
return Math.round(interval);
}
getName(): string {
return 'adaptive';
}
}
/**
* Create polling strategy from configuration
*/
export function createPollingStrategy(config: any): PollingStrategy {
const type = config.get('polling.strategy', 'adaptive');
const interval = config.get('polling.interval', 10000);
switch (type) {
case 'fixed':
return new FixedIntervalStrategy(interval);
default:
return new AdaptivePollingStrategy();
}
}

View File

@@ -0,0 +1,90 @@
import * as vscode from 'vscode';
import type { TaskMasterApi } from '../utils/task-master-api';
export class SidebarWebviewManager implements vscode.WebviewViewProvider {
private webviewView?: vscode.WebviewView;
private api?: TaskMasterApi;
constructor(private readonly extensionUri: vscode.Uri) {}
setApi(api: TaskMasterApi): void {
this.api = api;
// Update connection status if webview exists
if (this.webviewView) {
this.updateConnectionStatus();
}
}
resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
token: vscode.CancellationToken
): void {
this.webviewView = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [
vscode.Uri.joinPath(this.extensionUri, 'dist'),
vscode.Uri.joinPath(this.extensionUri, 'assets')
]
};
webviewView.webview.html = this.getHtmlContent(webviewView.webview);
// Handle messages from the webview
webviewView.webview.onDidReceiveMessage((message) => {
if (message.command === 'openBoard') {
vscode.commands.executeCommand('tm.showKanbanBoard');
}
});
// Update connection status on load
this.updateConnectionStatus();
}
updateConnectionStatus(): void {
if (!this.webviewView || !this.api) return;
const status = this.api.getConnectionStatus();
this.webviewView.webview.postMessage({
type: 'connectionStatus',
data: status
});
}
private getHtmlContent(webview: vscode.Webview): string {
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, 'dist', 'sidebar.js')
);
const styleUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, 'dist', 'index.css')
);
const nonce = this.getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}'; style-src ${webview.cspSource} 'unsafe-inline';">
<link href="${styleUri}" rel="stylesheet">
<title>TaskMaster</title>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}
private getNonce(): string {
let text = '';
const possible =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
}

View File

@@ -0,0 +1,168 @@
/**
* Task Repository - Simplified version
* Handles data access with caching
*/
import { EventEmitter } from '../utils/event-emitter';
import type { ExtensionLogger } from '../utils/logger';
import type { TaskMasterApi, TaskMasterTask } from '../utils/task-master-api';
// Use the TaskMasterTask type directly to ensure compatibility
export type Task = TaskMasterTask;
export class TaskRepository extends EventEmitter {
private cache: Task[] | null = null;
private cacheTimestamp = 0;
private readonly CACHE_DURATION = 30000; // 30 seconds
constructor(
private api: TaskMasterApi,
private logger: ExtensionLogger
) {
super();
}
async getAll(options?: {
tag?: string;
withSubtasks?: boolean;
}): Promise<Task[]> {
// If a tag is specified, always fetch fresh data
const shouldUseCache =
!options?.tag &&
this.cache &&
Date.now() - this.cacheTimestamp < this.CACHE_DURATION;
if (shouldUseCache) {
return this.cache || [];
}
try {
const result = await this.api.getTasks({
withSubtasks: options?.withSubtasks ?? true,
tag: options?.tag
});
if (result.success && result.data) {
this.cache = result.data;
this.cacheTimestamp = Date.now();
this.emit('tasks:updated', result.data);
return result.data;
}
throw new Error(result.error || 'Failed to fetch tasks');
} catch (error) {
this.logger.error('Failed to get tasks', error);
throw error;
}
}
async getById(taskId: string): Promise<Task | null> {
// First check cache
if (this.cache) {
// Handle both main tasks and subtasks
for (const task of this.cache) {
if (task.id === taskId) {
return task;
}
// Check subtasks
if (task.subtasks) {
for (const subtask of task.subtasks) {
if (
subtask.id.toString() === taskId ||
`${task.id}.${subtask.id}` === taskId
) {
return {
...subtask,
id: subtask.id.toString(),
description: subtask.description || '',
status: (subtask.status ||
'pending') as TaskMasterTask['status'],
priority: 'medium' as const,
dependencies:
subtask.dependencies?.map((d) => d.toString()) || []
};
}
}
}
}
}
// If not in cache, fetch all and search
const tasks = await this.getAll();
for (const task of tasks) {
if (task.id === taskId) {
return task;
}
// Check subtasks
if (task.subtasks) {
for (const subtask of task.subtasks) {
if (
subtask.id.toString() === taskId ||
`${task.id}.${subtask.id}` === taskId
) {
return {
...subtask,
id: subtask.id.toString(),
description: subtask.description || '',
status: (subtask.status || 'pending') as TaskMasterTask['status'],
priority: 'medium' as const,
dependencies: subtask.dependencies?.map((d) => d.toString()) || []
};
}
}
}
}
return null;
}
async updateStatus(taskId: string, status: Task['status']): Promise<void> {
try {
const result = await this.api.updateTaskStatus(taskId, status);
if (!result.success) {
throw new Error(result.error || 'Failed to update status');
}
// Invalidate cache
this.cache = null;
// Fetch updated tasks
await this.getAll();
} catch (error) {
this.logger.error('Failed to update task status', error);
throw error;
}
}
async updateContent(taskId: string, updates: any): Promise<void> {
try {
const result = await this.api.updateTask(taskId, updates, {
append: false,
research: false
});
if (!result.success) {
throw new Error(result.error || 'Failed to update task');
}
// Invalidate cache
this.cache = null;
// Fetch updated tasks
await this.getAll();
} catch (error) {
this.logger.error('Failed to update task content', error);
throw error;
}
}
async refresh(): Promise<void> {
this.cache = null;
await this.getAll();
}
isConnected(): boolean {
return this.api.getConnectionStatus().isConnected;
}
}

View File

@@ -0,0 +1,424 @@
/**
* Webview Manager - Simplified
* Manages webview panels and message handling
*/
import * as vscode from 'vscode';
import type { EventEmitter } from '../utils/event-emitter';
import type { ExtensionLogger } from '../utils/logger';
import type { ConfigService } from './config-service';
import type { TaskRepository } from './task-repository';
export class WebviewManager {
private panels = new Set<vscode.WebviewPanel>();
private configService?: ConfigService;
private mcpClient?: any;
private api?: any;
constructor(
private context: vscode.ExtensionContext,
private repository: TaskRepository,
private events: EventEmitter,
private logger: ExtensionLogger
) {}
setConfigService(configService: ConfigService): void {
this.configService = configService;
}
setMCPClient(mcpClient: any): void {
this.mcpClient = mcpClient;
}
setApi(api: any): void {
this.api = api;
}
async createOrShowPanel(): Promise<void> {
// Find existing panel
const existing = Array.from(this.panels).find(
(p) => p.title === 'TaskMaster Kanban'
);
if (existing) {
existing.reveal();
return;
}
// Create new panel
const panel = vscode.window.createWebviewPanel(
'taskrKanban',
'TaskMaster Kanban',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(this.context.extensionUri, 'dist')
]
}
);
// Set the icon for the webview tab
panel.iconPath = {
light: vscode.Uri.joinPath(
this.context.extensionUri,
'assets',
'icon-light.svg'
),
dark: vscode.Uri.joinPath(
this.context.extensionUri,
'assets',
'icon-dark.svg'
)
};
this.panels.add(panel);
panel.webview.html = this.getWebviewContent(panel.webview);
// Handle messages
panel.webview.onDidReceiveMessage(async (message) => {
await this.handleMessage(panel, message);
});
// Handle disposal
panel.onDidDispose(() => {
this.panels.delete(panel);
this.events.emit('webview:closed');
});
this.events.emit('webview:opened');
vscode.window.showInformationMessage('TaskMaster Kanban opened!');
}
broadcast(type: string, data: any): void {
this.panels.forEach((panel) => {
panel.webview.postMessage({ type, data });
});
}
getPanelCount(): number {
return this.panels.size;
}
dispose(): void {
this.panels.forEach((panel) => panel.dispose());
this.panels.clear();
}
private async handleMessage(
panel: vscode.WebviewPanel,
message: any
): Promise<void> {
// Validate message structure
if (!message || typeof message !== 'object') {
this.logger.error('Invalid message received:', message);
return;
}
const { type, data, requestId } = message;
this.logger.debug(`Webview message: ${type}`, message);
try {
let response: any;
switch (type) {
case 'ready':
// Webview is ready, send current connection status
const isConnected = this.mcpClient?.getStatus()?.isRunning || false;
panel.webview.postMessage({
type: 'connectionStatus',
data: {
isConnected: isConnected,
status: isConnected ? 'Connected' : 'Disconnected'
}
});
// No response needed for ready message
return;
case 'getTasks':
// Pass options to getAll including tag if specified
response = await this.repository.getAll({
tag: data?.tag,
withSubtasks: data?.withSubtasks ?? true
});
break;
case 'updateTaskStatus':
await this.repository.updateStatus(data.taskId, data.newStatus);
response = { success: true };
break;
case 'getConfig':
if (this.configService) {
response = await this.configService.getSafeConfig();
} else {
response = null;
}
break;
case 'readTaskFileData':
// For now, return the task data from repository
// In the future, this could read from actual task files
const task = await this.repository.getById(data.taskId);
if (task) {
response = {
details: task.details || '',
testStrategy: task.testStrategy || ''
};
} else {
response = {
details: '',
testStrategy: ''
};
}
break;
case 'updateTask':
// Handle task content updates with MCP
if (this.mcpClient) {
try {
const { taskId, updates, options = {} } = data;
// Use the update_task MCP tool
await this.mcpClient.callTool('update_task', {
id: String(taskId),
prompt: updates.description || '',
append: options.append || false,
research: options.research || false,
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
});
response = { success: true };
} catch (error) {
this.logger.error('Failed to update task via MCP:', error);
throw error;
}
} else {
throw new Error('MCP client not initialized');
}
break;
case 'updateSubtask':
// Handle subtask content updates with MCP
if (this.mcpClient) {
try {
const { taskId, prompt, options = {} } = data;
// Use the update_subtask MCP tool
await this.mcpClient.callTool('update_subtask', {
id: String(taskId),
prompt: prompt,
research: options.research || false,
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
});
response = { success: true };
} catch (error) {
this.logger.error('Failed to update subtask via MCP:', error);
throw error;
}
} else {
throw new Error('MCP client not initialized');
}
break;
case 'getComplexity':
// For backward compatibility - redirect to mcpRequest
this.logger.debug(
`getComplexity request for task ${data.taskId}, mcpClient available: ${!!this.mcpClient}`
);
if (this.mcpClient && data.taskId) {
try {
const complexityResult = await this.mcpClient.callTool(
'complexity_report',
{
projectRoot:
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
}
);
if (complexityResult?.report?.complexityAnalysis?.tasks) {
const task =
complexityResult.report.complexityAnalysis.tasks.find(
(t: any) => t.id === data.taskId
);
response = task ? { score: task.complexityScore } : {};
} else {
response = {};
}
} catch (error) {
this.logger.error('Failed to get complexity', error);
response = {};
}
} else {
this.logger.warn(
`Cannot get complexity: mcpClient=${!!this.mcpClient}, taskId=${data.taskId}`
);
response = {};
}
break;
case 'mcpRequest':
// Handle MCP tool calls
try {
// The tool and params come directly in the message
const tool = message.tool;
const params = message.params || {};
if (!this.mcpClient) {
throw new Error('MCP client not initialized');
}
if (!tool) {
throw new Error('Tool name not specified in mcpRequest');
}
// Add projectRoot if not provided
if (!params.projectRoot) {
params.projectRoot =
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
}
const result = await this.mcpClient.callTool(tool, params);
response = { data: result };
} catch (error) {
this.logger.error('MCP request failed:', error);
// Re-throw with cleaner error message
throw new Error(
error instanceof Error ? error.message : 'Unknown error'
);
}
break;
case 'getTags':
// Get available tags
if (this.mcpClient) {
try {
const result = await this.mcpClient.callTool('list_tags', {
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
showMetadata: false
});
// The MCP response has a specific structure
// Based on the MCP SDK, the response is in result.content[0].text
let parsedData;
if (
result?.content &&
Array.isArray(result.content) &&
result.content[0]?.text
) {
try {
parsedData = JSON.parse(result.content[0].text);
} catch (e) {
this.logger.error('Failed to parse MCP response text:', e);
}
}
// Extract tags data from the parsed response
if (parsedData?.data) {
response = parsedData.data;
} else if (parsedData) {
response = parsedData;
} else if (result?.data) {
response = result.data;
} else {
response = { tags: [], currentTag: 'master' };
}
} catch (error) {
this.logger.error('Failed to get tags:', error);
response = { tags: [], currentTag: 'master' };
}
} else {
response = { tags: [], currentTag: 'master' };
}
break;
case 'switchTag':
// Switch to a different tag
if (this.mcpClient && data.tagName) {
try {
await this.mcpClient.callTool('use_tag', {
name: data.tagName,
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
});
// Clear cache and fetch tasks for the new tag
await this.repository.refresh();
const tasks = await this.repository.getAll({ tag: data.tagName });
this.broadcast('tasksUpdated', { tasks, source: 'tag-switch' });
response = { success: true };
} catch (error) {
this.logger.error('Failed to switch tag:', error);
throw error;
}
} else {
throw new Error('Tag name not provided');
}
break;
case 'openExternal':
// Open external URL
if (message.url) {
vscode.env.openExternal(vscode.Uri.parse(message.url));
}
return;
default:
throw new Error(`Unknown message type: ${type}`);
}
// Send response
if (requestId) {
panel.webview.postMessage({
type: 'response',
requestId,
success: true,
data: response
});
}
} catch (error) {
this.logger.error(`Error handling message ${type}`, error);
if (requestId) {
panel.webview.postMessage({
type: 'error',
requestId,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}
private getWebviewContent(webview: vscode.Webview): string {
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'index.js')
);
const styleUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'index.css')
);
const nonce = this.getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}'; style-src ${webview.cspSource} 'unsafe-inline';">
<link href="${styleUri}" rel="stylesheet">
<title>TaskMaster Kanban</title>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}
private getNonce(): string {
let text = '';
const possible =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
}

View File

@@ -0,0 +1,15 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});

View File

@@ -0,0 +1,514 @@
import * as vscode from 'vscode';
import { logger } from './logger';
import type { MCPConfig } from './mcpClient';
export interface TaskMasterConfig {
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;
}
export interface UIConfig {
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;
}
export interface DebugConfig {
enableLogging: boolean;
logLevel: 'error' | 'warn' | 'info' | 'debug';
enableConnectionMetrics: boolean;
saveEventLogs: boolean;
maxEventLogSize: number;
}
export interface ConfigValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
}
export class ConfigManager {
private static instance: ConfigManager | null = null;
private config: TaskMasterConfig;
private configListeners: ((config: TaskMasterConfig) => void)[] = [];
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 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
};
}
/**
* 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 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 (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
);
}
}
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
);
}
}
this.config = newConfig;
this.notifyConfigChange();
}
/**
* Validate configuration
*/
validateConfig(config: TaskMasterConfig): ConfigValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// Validate MCP configuration
if (!config.mcp.command || config.mcp.command.trim() === '') {
errors.push('MCP command cannot be empty');
}
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.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 UI configuration
if (config.ui.refreshIntervalMs < 1000) {
warnings.push(
'UI refresh interval is very low (< 1s), this may impact performance'
);
}
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 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'
);
}
if (config.performance.requestTimeoutMs < 1000) {
warnings.push(
'Request timeout is very low (< 1s), this may cause premature timeouts'
);
}
// 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'
);
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
/**
* Reset configuration to defaults
*/
async resetToDefaults(): Promise<void> {
const defaultConfig = this.getDefaultConfig();
await this.updateConfig(defaultConfig);
}
/**
* Export configuration to JSON
*/
exportConfig(): string {
return JSON.stringify(this.config, null, 2);
}
/**
* 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(', ')}`
);
}
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;
}
}
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;
}
}
/**
* Add configuration change listener
*/
onConfigChange(listener: (config: TaskMasterConfig) => void): void {
this.configListeners.push(listener);
}
/**
* Remove configuration change listener
*/
removeConfigListener(listener: (config: TaskMasterConfig) => void): void {
const index = this.configListeners.indexOf(listener);
if (index !== -1) {
this.configListeners.splice(index, 1);
}
}
/**
* Load configuration from VS Code settings
*/
private loadConfig(): TaskMasterConfig {
const vsConfig = vscode.workspace.getConfiguration('taskmaster');
const defaultConfig = this.getDefaultConfig();
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: ['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')) {
logger.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) {
logger.error('Error in configuration change listener:', error);
}
});
}
}
/**
* Utility function to get configuration manager instance
*/
export function getConfigManager(): ConfigManager {
return ConfigManager.getInstance();
}

View File

@@ -0,0 +1,387 @@
import * as vscode from 'vscode';
import { logger } from './logger';
import {
MCPClientManager,
type MCPConfig,
type MCPServerStatus
} from './mcpClient';
export interface ConnectionEvent {
type: 'connected' | 'disconnected' | 'error' | 'reconnecting';
timestamp: Date;
data?: any;
}
export interface ConnectionHealth {
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;
// 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);
}
/**
* 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');
}
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() });
logger.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;
}
}
/**
* 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();
}
/**
* Get current connection status
*/
getStatus(): MCPServerStatus {
return this.mcpClient?.getStatus() || { isRunning: false };
}
/**
* Get connection health metrics
*/
getHealth(): ConnectionHealth {
this.updateHealth();
return { ...this.health };
}
/**
* Get recent connection events
*/
getEvents(limit = 10): ConnectionEvent[] {
return this.connectionEvents.slice(-limit);
}
/**
* 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'
};
}
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'
};
}
}
/**
* 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) {
logger.log(
'Multiple consecutive failures detected, attempting reconnection...'
);
this.reconnectWithBackoff().catch((err) => {
logger.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) {
logger.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) {
logger.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 * 2 ** (this.reconnectAttempts - 1),
this.maxBackoffMs
);
logger.log(
`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${backoffMs}ms...`
);
await new Promise((resolve) => setTimeout(resolve, backoffMs));
try {
await this.connect();
} catch (error) {
logger.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());
}
}
}

View File

@@ -0,0 +1,858 @@
import * as vscode from 'vscode';
import { logger } from './logger';
import {
getNotificationType,
getToastDuration,
shouldShowNotification
} from './notificationPreferences';
export enum ErrorSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export enum ErrorCategory {
MCP_CONNECTION = 'mcp_connection',
CONFIGURATION = 'configuration',
TASK_LOADING = 'task_loading',
UI_RENDERING = 'ui_rendering',
VALIDATION = 'validation',
NETWORK = 'network',
INTERNAL = 'internal',
TASK_MASTER_API = 'TASK_MASTER_API',
DATA_VALIDATION = 'DATA_VALIDATION',
DATA_PARSING = 'DATA_PARSING',
TASK_DATA_CORRUPTION = 'TASK_DATA_CORRUPTION',
VSCODE_API = 'VSCODE_API',
WEBVIEW = 'WEBVIEW',
EXTENSION_HOST = 'EXTENSION_HOST',
USER_INTERACTION = 'USER_INTERACTION',
DRAG_DROP = 'DRAG_DROP',
COMPONENT_RENDER = 'COMPONENT_RENDER',
PERMISSION = 'PERMISSION',
FILE_SYSTEM = 'FILE_SYSTEM',
UNKNOWN = 'UNKNOWN'
}
export enum NotificationType {
VSCODE_INFO = 'VSCODE_INFO',
VSCODE_WARNING = 'VSCODE_WARNING',
VSCODE_ERROR = 'VSCODE_ERROR',
TOAST_SUCCESS = 'TOAST_SUCCESS',
TOAST_INFO = 'TOAST_INFO',
TOAST_WARNING = 'TOAST_WARNING',
TOAST_ERROR = 'TOAST_ERROR',
CONSOLE_ONLY = 'CONSOLE_ONLY',
SILENT = 'SILENT'
}
export interface ErrorContext {
// Core error information
category: ErrorCategory;
severity: ErrorSeverity;
message: string;
originalError?: Error | unknown;
// Contextual information
operation?: string; // What operation was being performed
taskId?: string; // Related task ID if applicable
userId?: string; // User context if applicable
sessionId?: string; // Session context
// Technical details
stackTrace?: string;
userAgent?: string;
timestamp?: number;
// Recovery information
isRecoverable?: boolean;
suggestedActions?: string[];
documentationLink?: string;
// Notification preferences
notificationType?: NotificationType;
showToUser?: boolean;
logToConsole?: boolean;
logToFile?: boolean;
}
export interface ErrorDetails {
code: string;
message: string;
category: ErrorCategory;
severity: ErrorSeverity;
timestamp: Date;
context?: Record<string, any>;
stack?: string;
userAction?: string;
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
};
}
export interface ErrorLogEntry {
id: string;
error: ErrorDetails;
resolved: boolean;
resolvedAt?: Date;
attempts: number;
lastAttempt?: Date;
}
/**
* Base class for all Task Master errors
*/
export abstract class TaskMasterError extends Error {
public readonly code: string;
public readonly category: ErrorCategory;
public readonly severity: ErrorSeverity;
public readonly timestamp: Date;
public readonly context?: Record<string, any>;
public readonly userAction?: string;
public readonly recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
};
constructor(
message: string,
code: string,
category: ErrorCategory,
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
context?: Record<string, any>,
userAction?: string,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.category = category;
this.severity = severity;
this.timestamp = new Date();
this.context = context;
this.userAction = userAction;
this.recovery = recovery;
// Capture stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
public toErrorDetails(): ErrorDetails {
return {
code: this.code,
message: this.message,
category: this.category,
severity: this.severity,
timestamp: this.timestamp,
context: this.context,
stack: this.stack,
userAction: this.userAction,
recovery: this.recovery
};
}
}
/**
* MCP Connection related errors
*/
export class MCPConnectionError extends TaskMasterError {
constructor(
message: string,
code = 'MCP_CONNECTION_FAILED',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.MCP_CONNECTION,
ErrorSeverity.HIGH,
context,
'Check your Task Master configuration and ensure the MCP server is accessible.',
recovery
);
}
}
/**
* Configuration related errors
*/
export class ConfigurationError extends TaskMasterError {
constructor(
message: string,
code = 'CONFIGURATION_INVALID',
context?: Record<string, any>
) {
super(
message,
code,
ErrorCategory.CONFIGURATION,
ErrorSeverity.MEDIUM,
context,
'Check your Task Master configuration in VS Code settings.'
);
}
}
/**
* Task loading related errors
*/
export class TaskLoadingError extends TaskMasterError {
constructor(
message: string,
code = 'TASK_LOADING_FAILED',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.TASK_LOADING,
ErrorSeverity.MEDIUM,
context,
'Try refreshing the task list or check your project configuration.',
recovery
);
}
}
/**
* UI rendering related errors
*/
export class UIRenderingError extends TaskMasterError {
constructor(
message: string,
code = 'UI_RENDERING_FAILED',
context?: Record<string, any>
) {
super(
message,
code,
ErrorCategory.UI_RENDERING,
ErrorSeverity.LOW,
context,
'Try closing and reopening the Kanban board.'
);
}
}
/**
* Network related errors
*/
export class NetworkError extends TaskMasterError {
constructor(
message: string,
code = 'NETWORK_ERROR',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.NETWORK,
ErrorSeverity.MEDIUM,
context,
'Check your network connection and firewall settings.',
recovery
);
}
}
/**
* Centralized error handler
*/
export class ErrorHandler {
private static instance: ErrorHandler | null = null;
private errorLog: ErrorLogEntry[] = [];
private maxLogSize = 1000;
private errorListeners: ((error: ErrorDetails) => void)[] = [];
private constructor() {
this.setupGlobalErrorHandlers();
}
static getInstance(): ErrorHandler {
if (!ErrorHandler.instance) {
ErrorHandler.instance = new ErrorHandler();
}
return ErrorHandler.instance;
}
/**
* Handle an error with comprehensive logging and recovery
*/
async handleError(
error: Error | TaskMasterError,
context?: Record<string, any>
): Promise<void> {
const errorDetails = this.createErrorDetails(error, context);
const logEntry = this.logError(errorDetails);
// Notify listeners
this.notifyErrorListeners(errorDetails);
// Show user notification based on severity
await this.showUserNotification(errorDetails);
// Attempt recovery if available
if (errorDetails.recovery?.automatic && errorDetails.recovery.action) {
try {
await errorDetails.recovery.action();
this.markErrorResolved(logEntry.id);
} catch (recoveryError) {
logger.error('Error recovery failed:', recoveryError);
logEntry.attempts++;
logEntry.lastAttempt = new Date();
}
}
// Log to console with appropriate level
this.logToConsole(errorDetails);
}
/**
* Handle critical errors that should stop execution
*/
async handleCriticalError(
error: Error | TaskMasterError,
context?: Record<string, any>
): Promise<void> {
const errorDetails = this.createErrorDetails(error, context);
errorDetails.severity = ErrorSeverity.CRITICAL;
await this.handleError(error, context);
// Show critical error dialog
const action = await vscode.window.showErrorMessage(
`Critical Error in Task Master: ${errorDetails.message}`,
'View Details',
'Report Issue',
'Restart Extension'
);
switch (action) {
case 'View Details':
await this.showErrorDetails(errorDetails);
break;
case 'Report Issue':
await this.openIssueReport(errorDetails);
break;
case 'Restart Extension':
await vscode.commands.executeCommand('workbench.action.reloadWindow');
break;
}
}
/**
* Add error event listener
*/
onError(listener: (error: ErrorDetails) => void): void {
this.errorListeners.push(listener);
}
/**
* Remove error event listener
*/
removeErrorListener(listener: (error: ErrorDetails) => void): void {
const index = this.errorListeners.indexOf(listener);
if (index !== -1) {
this.errorListeners.splice(index, 1);
}
}
/**
* Get error log
*/
getErrorLog(
category?: ErrorCategory,
severity?: ErrorSeverity
): ErrorLogEntry[] {
let filteredLog = this.errorLog;
if (category) {
filteredLog = filteredLog.filter(
(entry) => entry.error.category === category
);
}
if (severity) {
filteredLog = filteredLog.filter(
(entry) => entry.error.severity === severity
);
}
return filteredLog.slice().reverse(); // Most recent first
}
/**
* Clear error log
*/
clearErrorLog(): void {
this.errorLog = [];
}
/**
* Export error log for debugging
*/
exportErrorLog(): string {
return JSON.stringify(this.errorLog, null, 2);
}
/**
* Create error details from error instance
*/
private createErrorDetails(
error: Error | TaskMasterError,
context?: Record<string, any>
): ErrorDetails {
if (error instanceof TaskMasterError) {
const details = error.toErrorDetails();
if (context) {
details.context = { ...details.context, ...context };
}
return details;
}
// Handle standard Error objects
return {
code: 'UNKNOWN_ERROR',
message: error.message || 'An unknown error occurred',
category: ErrorCategory.INTERNAL,
severity: ErrorSeverity.MEDIUM,
timestamp: new Date(),
context: { ...context, errorName: error.name },
stack: error.stack
};
}
/**
* Log error to internal log
*/
private logError(errorDetails: ErrorDetails): ErrorLogEntry {
const logEntry: ErrorLogEntry = {
id: `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
error: errorDetails,
resolved: false,
attempts: 0
};
this.errorLog.push(logEntry);
// Maintain log size limit
if (this.errorLog.length > this.maxLogSize) {
this.errorLog = this.errorLog.slice(-this.maxLogSize);
}
return logEntry;
}
/**
* Mark error as resolved
*/
private markErrorResolved(errorId: string): void {
const entry = this.errorLog.find((e) => e.id === errorId);
if (entry) {
entry.resolved = true;
entry.resolvedAt = new Date();
}
}
/**
* Show user notification based on error severity and user preferences
*/
private async showUserNotification(
errorDetails: ErrorDetails
): Promise<void> {
// Check if user wants to see this notification
if (!shouldShowNotification(errorDetails.category, errorDetails.severity)) {
return;
}
const notificationType = getNotificationType(
errorDetails.category,
errorDetails.severity
);
const message = errorDetails.userAction
? `${errorDetails.message} ${errorDetails.userAction}`
: errorDetails.message;
// Handle different notification types based on user preferences
switch (notificationType) {
case 'VSCODE_ERROR':
await vscode.window.showErrorMessage(message);
break;
case 'VSCODE_WARNING':
await vscode.window.showWarningMessage(message);
break;
case 'VSCODE_INFO':
await vscode.window.showInformationMessage(message);
break;
case 'TOAST_SUCCESS':
case 'TOAST_INFO':
case 'TOAST_WARNING':
case 'TOAST_ERROR':
// These will be handled by the webview toast system
// The error listener in extension.ts will send these to webview
break;
case 'CONSOLE_ONLY':
case 'SILENT':
// No user notification, just console logging
break;
default:
// Fallback to severity-based notifications
switch (errorDetails.severity) {
case ErrorSeverity.CRITICAL:
await vscode.window.showErrorMessage(message);
break;
case ErrorSeverity.HIGH:
await vscode.window.showErrorMessage(message);
break;
case ErrorSeverity.MEDIUM:
await vscode.window.showWarningMessage(message);
break;
case ErrorSeverity.LOW:
await vscode.window.showInformationMessage(message);
break;
}
}
}
/**
* Log to console with appropriate level
*/
private logToConsole(errorDetails: ErrorDetails): void {
const logMessage = `[${errorDetails.category}] ${errorDetails.code}: ${errorDetails.message}`;
switch (errorDetails.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
logger.error(logMessage, errorDetails);
break;
case ErrorSeverity.MEDIUM:
logger.warn(logMessage, errorDetails);
break;
case ErrorSeverity.LOW:
console.info(logMessage, errorDetails);
break;
}
}
/**
* Show detailed error information
*/
private async showErrorDetails(errorDetails: ErrorDetails): Promise<void> {
const details = [
`Error Code: ${errorDetails.code}`,
`Category: ${errorDetails.category}`,
`Severity: ${errorDetails.severity}`,
`Time: ${errorDetails.timestamp.toISOString()}`,
`Message: ${errorDetails.message}`
];
if (errorDetails.context) {
details.push(`Context: ${JSON.stringify(errorDetails.context, null, 2)}`);
}
if (errorDetails.stack) {
details.push(`Stack Trace: ${errorDetails.stack}`);
}
const content = details.join('\n\n');
// Create temporary document to show error details
const doc = await vscode.workspace.openTextDocument({
content,
language: 'plaintext'
});
await vscode.window.showTextDocument(doc);
}
/**
* Open GitHub issue report
*/
private async openIssueReport(errorDetails: ErrorDetails): Promise<void> {
const issueTitle = encodeURIComponent(
`Error: ${errorDetails.code} - ${errorDetails.message}`
);
const issueBody = encodeURIComponent(`
**Error Details:**
- Code: ${errorDetails.code}
- Category: ${errorDetails.category}
- Severity: ${errorDetails.severity}
- Time: ${errorDetails.timestamp.toISOString()}
**Message:**
${errorDetails.message}
**Context:**
${errorDetails.context ? JSON.stringify(errorDetails.context, null, 2) : 'None'}
**Steps to Reproduce:**
1.
2.
3.
**Expected Behavior:**
**Additional Notes:**
`);
const issueUrl = `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${issueTitle}&body=${issueBody}`;
await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
}
/**
* Notify error listeners
*/
private notifyErrorListeners(errorDetails: ErrorDetails): void {
this.errorListeners.forEach((listener) => {
try {
listener(errorDetails);
} catch (error) {
logger.error('Error in error listener:', error);
}
});
}
/**
* Setup global error handlers
*/
private setupGlobalErrorHandlers(): void {
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
// Create a concrete error class for internal errors
class InternalError extends TaskMasterError {
constructor(
message: string,
code: string,
severity: ErrorSeverity,
context?: Record<string, any>
) {
super(message, code, ErrorCategory.INTERNAL, severity, context);
}
}
const error = new InternalError(
'Unhandled Promise Rejection',
'UNHANDLED_REJECTION',
ErrorSeverity.HIGH,
{ reason: String(reason), promise: String(promise) }
);
this.handleError(error);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
// Create a concrete error class for internal errors
class InternalError extends TaskMasterError {
constructor(
message: string,
code: string,
severity: ErrorSeverity,
context?: Record<string, any>
) {
super(message, code, ErrorCategory.INTERNAL, severity, context);
}
}
const taskMasterError = new InternalError(
'Uncaught Exception',
'UNCAUGHT_EXCEPTION',
ErrorSeverity.CRITICAL,
{ originalError: error.message, stack: error.stack }
);
this.handleCriticalError(taskMasterError);
});
}
}
/**
* Utility functions for error handling
*/
export function getErrorHandler(): ErrorHandler {
return ErrorHandler.getInstance();
}
export function createRecoveryAction(
action: () => Promise<void>,
description: string
) {
return {
automatic: false,
action,
description
};
}
export function createAutoRecoveryAction(
action: () => Promise<void>,
description: string
) {
return {
automatic: true,
action,
description
};
}
// Default error categorization rules
export const ERROR_CATEGORIZATION_RULES: Record<string, ErrorCategory> = {
// Network patterns
ECONNREFUSED: ErrorCategory.NETWORK,
ENOTFOUND: ErrorCategory.NETWORK,
ETIMEDOUT: ErrorCategory.NETWORK,
'Network request failed': ErrorCategory.NETWORK,
'fetch failed': ErrorCategory.NETWORK,
// MCP patterns
MCP: ErrorCategory.MCP_CONNECTION,
'Task Master': ErrorCategory.TASK_MASTER_API,
polling: ErrorCategory.TASK_MASTER_API,
// VS Code patterns
vscode: ErrorCategory.VSCODE_API,
webview: ErrorCategory.WEBVIEW,
extension: ErrorCategory.EXTENSION_HOST,
// Data patterns
JSON: ErrorCategory.DATA_PARSING,
parse: ErrorCategory.DATA_PARSING,
validation: ErrorCategory.DATA_VALIDATION,
invalid: ErrorCategory.DATA_VALIDATION,
// Permission patterns
EACCES: ErrorCategory.PERMISSION,
EPERM: ErrorCategory.PERMISSION,
permission: ErrorCategory.PERMISSION,
// File system patterns
ENOENT: ErrorCategory.FILE_SYSTEM,
EISDIR: ErrorCategory.FILE_SYSTEM,
file: ErrorCategory.FILE_SYSTEM
};
// Severity mapping based on error categories
export const CATEGORY_SEVERITY_MAPPING: Record<ErrorCategory, ErrorSeverity> = {
[ErrorCategory.NETWORK]: ErrorSeverity.MEDIUM,
[ErrorCategory.MCP_CONNECTION]: ErrorSeverity.HIGH,
[ErrorCategory.TASK_MASTER_API]: ErrorSeverity.HIGH,
[ErrorCategory.DATA_VALIDATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.DATA_PARSING]: ErrorSeverity.HIGH,
[ErrorCategory.TASK_DATA_CORRUPTION]: ErrorSeverity.CRITICAL,
[ErrorCategory.VSCODE_API]: ErrorSeverity.HIGH,
[ErrorCategory.WEBVIEW]: ErrorSeverity.MEDIUM,
[ErrorCategory.EXTENSION_HOST]: ErrorSeverity.CRITICAL,
[ErrorCategory.USER_INTERACTION]: ErrorSeverity.LOW,
[ErrorCategory.DRAG_DROP]: ErrorSeverity.MEDIUM,
[ErrorCategory.COMPONENT_RENDER]: ErrorSeverity.MEDIUM,
[ErrorCategory.PERMISSION]: ErrorSeverity.CRITICAL,
[ErrorCategory.FILE_SYSTEM]: ErrorSeverity.HIGH,
[ErrorCategory.CONFIGURATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.UNKNOWN]: ErrorSeverity.HIGH,
// Legacy mappings for existing categories
[ErrorCategory.TASK_LOADING]: ErrorSeverity.HIGH,
[ErrorCategory.UI_RENDERING]: ErrorSeverity.MEDIUM,
[ErrorCategory.VALIDATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.INTERNAL]: ErrorSeverity.HIGH
};
// Notification type mapping based on severity
export const SEVERITY_NOTIFICATION_MAPPING: Record<
ErrorSeverity,
NotificationType
> = {
[ErrorSeverity.LOW]: NotificationType.TOAST_INFO,
[ErrorSeverity.MEDIUM]: NotificationType.TOAST_WARNING,
[ErrorSeverity.HIGH]: NotificationType.VSCODE_WARNING,
[ErrorSeverity.CRITICAL]: NotificationType.VSCODE_ERROR
};
/**
* Automatically categorize an error based on its message and type
*/
export function categorizeError(
error: Error | unknown,
operation?: string
): ErrorCategory {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
const searchText =
`${errorMessage} ${errorStack || ''} ${operation || ''}`.toLowerCase();
for (const [pattern, category] of Object.entries(
ERROR_CATEGORIZATION_RULES
)) {
if (searchText.includes(pattern.toLowerCase())) {
return category;
}
}
return ErrorCategory.UNKNOWN;
}
export function getSuggestedSeverity(category: ErrorCategory): ErrorSeverity {
return CATEGORY_SEVERITY_MAPPING[category] || ErrorSeverity.HIGH;
}
export function getSuggestedNotificationType(
severity: ErrorSeverity
): NotificationType {
return (
SEVERITY_NOTIFICATION_MAPPING[severity] || NotificationType.CONSOLE_ONLY
);
}
export function createErrorContext(
error: Error | unknown,
operation?: string,
overrides?: Partial<ErrorContext>
): ErrorContext {
const category = categorizeError(error, operation);
const severity = getSuggestedSeverity(category);
const notificationType = getSuggestedNotificationType(severity);
const baseContext: ErrorContext = {
category,
severity,
message: error instanceof Error ? error.message : String(error),
originalError: error,
operation,
timestamp: Date.now(),
stackTrace: error instanceof Error ? error.stack : undefined,
isRecoverable: severity !== ErrorSeverity.CRITICAL,
notificationType,
showToUser:
severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL,
logToConsole: true,
logToFile:
severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL
};
return { ...baseContext, ...overrides };
}

View File

@@ -0,0 +1,34 @@
/**
* Simple Event Emitter
* Lightweight alternative to complex event bus
*/
export type EventHandler = (...args: any[]) => void | Promise<void>;
export class EventEmitter {
private handlers = new Map<string, Set<EventHandler>>();
on(event: string, handler: EventHandler): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)?.add(handler);
// Return unsubscribe function
return () => this.off(event, handler);
}
off(event: string, handler: EventHandler): void {
this.handlers.get(event)?.delete(handler);
}
emit(event: string, ...args: any[]): void {
this.handlers.get(event)?.forEach((handler) => {
try {
handler(...args);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
}

View File

@@ -0,0 +1,104 @@
import * as vscode from 'vscode';
/**
* Logger interface for dependency injection
*/
export interface ILogger {
log(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
debug(message: string, ...args: any[]): void;
show(): void;
dispose(): void;
}
/**
* Logger that outputs to VS Code's output channel instead of console
* This prevents interference with MCP stdio communication
*/
export class ExtensionLogger implements ILogger {
private static instance: ExtensionLogger;
private outputChannel: vscode.OutputChannel;
private debugMode: boolean;
private constructor() {
this.outputChannel = vscode.window.createOutputChannel('TaskMaster');
const config = vscode.workspace.getConfiguration('taskmaster');
this.debugMode = config.get<boolean>('debug.enableLogging', true);
}
static getInstance(): ExtensionLogger {
if (!ExtensionLogger.instance) {
ExtensionLogger.instance = new ExtensionLogger();
}
return ExtensionLogger.instance;
}
log(message: string, ...args: any[]): void {
if (!this.debugMode) {
return;
}
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(message, args);
this.outputChannel.appendLine(`[${timestamp}] ${formattedMessage}`);
}
error(message: string, ...args: any[]): void {
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(message, args);
this.outputChannel.appendLine(`[${timestamp}] ERROR: ${formattedMessage}`);
}
warn(message: string, ...args: any[]): void {
if (!this.debugMode) {
return;
}
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(message, args);
this.outputChannel.appendLine(`[${timestamp}] WARN: ${formattedMessage}`);
}
debug(message: string, ...args: any[]): void {
if (!this.debugMode) {
return;
}
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(message, args);
this.outputChannel.appendLine(`[${timestamp}] DEBUG: ${formattedMessage}`);
}
private formatMessage(message: string, args: any[]): string {
if (args.length === 0) {
return message;
}
// Convert objects to JSON for better readability
const formattedArgs = args.map((arg) => {
if (typeof arg === 'object' && arg !== null) {
try {
return JSON.stringify(arg, null, 2);
} catch {
return String(arg);
}
}
return String(arg);
});
return `${message} ${formattedArgs.join(' ')}`;
}
show(): void {
this.outputChannel.show();
}
dispose(): void {
this.outputChannel.dispose();
}
setDebugMode(enabled: boolean): void {
this.debugMode = enabled;
}
}
// Export a singleton instance for convenience
export const logger = ExtensionLogger.getInstance();

View File

@@ -0,0 +1,390 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import * as vscode from 'vscode';
import { logger } from './logger';
export interface MCPConfig {
command: string;
args: string[];
cwd?: string;
env?: Record<string, string>;
timeout?: number;
}
export interface MCPServerStatus {
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;
constructor(config: MCPConfig) {
logger.log(
'🔍 DEBUGGING: MCPClientManager constructor called with config:',
config
);
this.config = config;
}
/**
* 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;
}
this.connectionPromise = this._doConnect();
return this.connectionPromise;
}
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
logger.log(
`Starting MCP server: ${this.config.command} ${this.config.args?.join(' ') || ''}`
);
logger.log('🔍 DEBUGGING: Transport config cwd:', this.config.cwd);
logger.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');
logger.log(
'🔍 DEBUGGING: Checking target directory:',
targetDir,
'exists:',
fs.existsSync(targetDir)
);
logger.log(
'🔍 DEBUGGING: Checking .taskmaster dir:',
taskmasterDir,
'exists:',
fs.existsSync(taskmasterDir)
);
logger.log(
'🔍 DEBUGGING: Checking tasks.json:',
tasksFile,
'exists:',
fs.existsSync(tasksFile)
);
if (fs.existsSync(tasksFile)) {
const stats = fs.statSync(tasksFile);
logger.log('🔍 DEBUGGING: tasks.json size:', stats.size, 'bytes');
}
} catch (error) {
logger.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
}
});
logger.log('🔍 DEBUGGING: Transport created, checking process...');
// Set up transport event handlers
this.transport.onerror = (error: Error) => {
logger.error('❌ MCP transport error:', error);
logger.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(
`TaskMaster MCP transport error: ${error.message}`
);
};
this.transport.onclose = () => {
logger.log('🔌 MCP transport closed');
this.status = { isRunning: false };
this.client = null;
this.transport = null;
};
// Add message handler like the working debug script
this.transport.onmessage = (message: any) => {
logger.log('📤 MCP server message:', message);
};
// Create the client
this.client = new Client(
{
name: 'taskr-vscode-extension',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
);
// Connect the client to the transport (this automatically starts the transport)
logger.log('🔄 Attempting MCP client connection...');
logger.log('MCP config:', {
command: this.config.command,
args: this.config.args,
cwd: this.config.cwd
});
logger.log('Current working directory:', process.cwd());
logger.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;
logger.log('📝 MCP server process PID:', proc.pid);
logger.log('📝 Process working directory will be:', this.config.cwd);
proc.on('exit', (code: number, signal: string) => {
logger.log(
`🔚 MCP server process exited with code ${code}, signal ${signal}`
);
if (code !== 0) {
logger.log('❌ Non-zero exit code indicates server failure');
}
});
proc.on('error', (error: Error) => {
logger.log('❌ MCP server process error:', error);
});
// Listen to stderr to see server-side errors
if (proc.stderr) {
proc.stderr.on('data', (data: Buffer) => {
logger.log('📥 MCP server stderr:', data.toString());
});
}
// Listen to stdout for server messages
if (proc.stdout) {
proc.stdout.on('data', (data: Buffer) => {
logger.log('📤 MCP server stdout:', data.toString());
});
}
} else {
logger.log('⚠️ No process found in transport before connection');
}
await this.client.connect(this.transport);
// Update status
this.status = {
isRunning: true,
pid: this.transport.pid || undefined
};
logger.log('MCP client connected successfully');
} catch (error) {
logger.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;
}
}
/**
* Disconnect from the MCP server and clean up resources
*/
async disconnect(): Promise<void> {
logger.log('Disconnecting from MCP server');
if (this.client) {
try {
await this.client.close();
} catch (error) {
logger.error('Error closing MCP client:', error);
}
this.client = null;
}
if (this.transport) {
try {
await this.transport.close();
} catch (error) {
logger.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 {
// Use the configured timeout or default to 5 minutes
const timeout = this.config.timeout || 300000; // 5 minutes default
logger.log(`Calling MCP tool "${toolName}" with timeout: ${timeout}ms`);
const result = await this.client.callTool(
{
name: toolName,
arguments: arguments_
},
undefined,
{
timeout: timeout
}
);
return result;
} catch (error) {
logger.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;
}
// listTools is a simple metadata request, no need for extended timeout
const result = await this.client.listTools();
logger.log(
'Available MCP tools:',
result.tools?.map((t) => t.name) || []
);
return true;
} catch (error) {
logger.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 {
logger.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', ['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');
const timeout = config.get<number>('mcp.requestTimeoutMs', 300000);
logger.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;
logger.log(`✅ Using npx at: ${path}`);
break;
}
} catch (error) {
// Continue to next path
}
}
}
return {
command,
args,
cwd: cwd || defaultCwd,
env,
timeout
};
}

View File

@@ -0,0 +1,463 @@
import * as vscode from 'vscode';
import { ErrorCategory, ErrorSeverity, NotificationType } from './errorHandler';
import { logger } from './logger';
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;
}
export class NotificationPreferencesManager {
private static instance: NotificationPreferencesManager | null = null;
private readonly configSection = 'taskMasterKanban';
private constructor() {}
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
)
};
}
/**
* 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
);
}
}
/**
* 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
);
logger.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();
}
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 getNotificationType(
category: ErrorCategory,
severity: ErrorSeverity
): NotificationType {
return NotificationPreferencesManager.getInstance().getNotificationType(
category,
severity
);
}
export function getToastDuration(severity: ErrorSeverity): number {
return NotificationPreferencesManager.getInstance().getToastDuration(
severity
);
}

View File

@@ -0,0 +1,253 @@
/**
* Cache Manager
* Handles all caching logic with LRU eviction and analytics
*/
import type { ExtensionLogger } from '../../logger';
import type { CacheAnalytics, CacheConfig, CacheEntry } from '../types';
export class CacheManager {
private cache = new Map<string, CacheEntry>();
private analytics: CacheAnalytics = {
hits: 0,
misses: 0,
evictions: 0,
refreshes: 0,
totalSize: 0,
averageAccessTime: 0,
hitRate: 0
};
private backgroundRefreshTimer?: NodeJS.Timeout;
constructor(
private config: CacheConfig & { cacheDuration: number },
private logger: ExtensionLogger
) {
if (config.enableBackgroundRefresh) {
this.initializeBackgroundRefresh();
}
}
/**
* Get data from cache if not expired
*/
get(key: string): any {
const startTime = Date.now();
const cached = this.cache.get(key);
if (cached) {
const isExpired =
Date.now() - cached.timestamp >=
(cached.ttl || this.config.cacheDuration);
if (!isExpired) {
// Update access statistics
cached.accessCount++;
cached.lastAccessed = Date.now();
if (this.config.enableAnalytics) {
this.analytics.hits++;
}
const accessTime = Date.now() - startTime;
this.logger.debug(
`Cache hit for ${key} (${accessTime}ms, ${cached.accessCount} accesses)`
);
return cached.data;
} else {
// Remove expired entry
this.cache.delete(key);
this.logger.debug(`Cache entry expired and removed: ${key}`);
}
}
if (this.config.enableAnalytics) {
this.analytics.misses++;
}
this.logger.debug(`Cache miss for ${key}`);
return null;
}
/**
* Set data in cache with LRU eviction
*/
set(
key: string,
data: any,
options?: { ttl?: number; tags?: string[] }
): void {
const now = Date.now();
const dataSize = this.estimateDataSize(data);
// Create cache entry
const entry: CacheEntry = {
data,
timestamp: now,
accessCount: 1,
lastAccessed: now,
size: dataSize,
ttl: options?.ttl,
tags: options?.tags || [key.split('_')[0]]
};
// Check if we need to evict entries (LRU strategy)
if (this.cache.size >= this.config.maxSize) {
this.evictLRUEntries(Math.max(1, Math.floor(this.config.maxSize * 0.1)));
}
this.cache.set(key, entry);
this.logger.debug(
`Cached data for ${key} (size: ${dataSize} bytes, TTL: ${entry.ttl || this.config.cacheDuration}ms)`
);
// Trigger prefetch if enabled
if (this.config.enablePrefetch) {
this.scheduleRelatedDataPrefetch(key, data);
}
}
/**
* Clear cache entries matching a pattern
*/
clearPattern(pattern: string): void {
let evictedCount = 0;
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
evictedCount++;
}
}
if (evictedCount > 0) {
this.analytics.evictions += evictedCount;
this.logger.debug(
`Evicted ${evictedCount} cache entries matching pattern: ${pattern}`
);
}
}
/**
* Clear all cached data
*/
clear(): void {
this.cache.clear();
this.resetAnalytics();
}
/**
* Get cache analytics
*/
getAnalytics(): CacheAnalytics {
this.updateAnalytics();
return { ...this.analytics };
}
/**
* Get frequently accessed entries for background refresh
*/
getRefreshCandidates(): Array<[string, CacheEntry]> {
return Array.from(this.cache.entries())
.filter(([key, entry]) => {
const age = Date.now() - entry.timestamp;
const isNearExpiration = age > this.config.cacheDuration * 0.7;
const isFrequentlyAccessed = entry.accessCount >= 3;
return (
isNearExpiration && isFrequentlyAccessed && key.includes('get_tasks')
);
})
.sort((a, b) => b[1].accessCount - a[1].accessCount)
.slice(0, 5);
}
/**
* Update refresh count for analytics
*/
incrementRefreshes(): void {
this.analytics.refreshes++;
}
/**
* Cleanup resources
*/
destroy(): void {
if (this.backgroundRefreshTimer) {
clearInterval(this.backgroundRefreshTimer);
this.backgroundRefreshTimer = undefined;
}
this.clear();
}
private initializeBackgroundRefresh(): void {
if (this.backgroundRefreshTimer) {
clearInterval(this.backgroundRefreshTimer);
}
const interval = this.config.refreshInterval;
this.backgroundRefreshTimer = setInterval(() => {
// Background refresh is handled by the main API class
// This just maintains the timer
}, interval);
this.logger.debug(
`Cache background refresh initialized with ${interval}ms interval`
);
}
private evictLRUEntries(count: number): void {
const entries = Array.from(this.cache.entries())
.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)
.slice(0, count);
for (const [key] of entries) {
this.cache.delete(key);
this.analytics.evictions++;
}
if (entries.length > 0) {
this.logger.debug(`Evicted ${entries.length} LRU cache entries`);
}
}
private estimateDataSize(data: any): number {
try {
return JSON.stringify(data).length * 2; // Rough estimate
} catch {
return 1000; // Default fallback
}
}
private scheduleRelatedDataPrefetch(key: string, data: any): void {
if (key.includes('get_tasks') && Array.isArray(data)) {
this.logger.debug(
`Scheduled prefetch for ${data.length} tasks related to ${key}`
);
}
}
private resetAnalytics(): void {
this.analytics = {
hits: 0,
misses: 0,
evictions: 0,
refreshes: 0,
totalSize: 0,
averageAccessTime: 0,
hitRate: 0
};
}
private updateAnalytics(): void {
const total = this.analytics.hits + this.analytics.misses;
this.analytics.hitRate = total > 0 ? this.analytics.hits / total : 0;
this.analytics.totalSize = this.cache.size;
if (this.cache.size > 0) {
const totalAccessTime = Array.from(this.cache.values()).reduce(
(sum, entry) => sum + (entry.lastAccessed - entry.timestamp),
0
);
this.analytics.averageAccessTime = totalAccessTime / this.cache.size;
}
}
}

View File

@@ -0,0 +1,471 @@
/**
* TaskMaster API
* Main API class that coordinates all modules
*/
import * as vscode from 'vscode';
import { ExtensionLogger } from '../logger';
import type { MCPClientManager } from '../mcpClient';
import { CacheManager } from './cache/cache-manager';
import { MCPClient } from './mcp-client';
import { TaskTransformer } from './transformers/task-transformer';
import type {
AddSubtaskOptions,
CacheConfig,
GetTasksOptions,
SubtaskData,
TaskMasterApiConfig,
TaskMasterApiResponse,
TaskMasterTask,
TaskUpdate,
UpdateSubtaskOptions,
UpdateTaskOptions,
UpdateTaskStatusOptions
} from './types';
// Re-export types for backward compatibility
export * from './types';
export class TaskMasterApi {
private mcpWrapper: MCPClient;
private cache: CacheManager;
private transformer: TaskTransformer;
private config: TaskMasterApiConfig;
private logger: ExtensionLogger;
private readonly defaultCacheConfig: CacheConfig = {
maxSize: 100,
enableBackgroundRefresh: true,
refreshInterval: 5 * 60 * 1000, // 5 minutes
enableAnalytics: true,
enablePrefetch: true,
compressionEnabled: false,
persistToDisk: false
};
private readonly defaultConfig: TaskMasterApiConfig = {
timeout: 30000,
retryAttempts: 3,
cacheDuration: 5 * 60 * 1000, // 5 minutes
cache: this.defaultCacheConfig
};
constructor(
mcpClient: MCPClientManager,
config?: Partial<TaskMasterApiConfig>
) {
this.logger = ExtensionLogger.getInstance();
// Merge config - ensure cache is always fully defined
const mergedCache: CacheConfig = {
maxSize: config?.cache?.maxSize ?? this.defaultCacheConfig.maxSize,
enableBackgroundRefresh:
config?.cache?.enableBackgroundRefresh ??
this.defaultCacheConfig.enableBackgroundRefresh,
refreshInterval:
config?.cache?.refreshInterval ??
this.defaultCacheConfig.refreshInterval,
enableAnalytics:
config?.cache?.enableAnalytics ??
this.defaultCacheConfig.enableAnalytics,
enablePrefetch:
config?.cache?.enablePrefetch ?? this.defaultCacheConfig.enablePrefetch,
compressionEnabled:
config?.cache?.compressionEnabled ??
this.defaultCacheConfig.compressionEnabled,
persistToDisk:
config?.cache?.persistToDisk ?? this.defaultCacheConfig.persistToDisk
};
this.config = {
...this.defaultConfig,
...config,
cache: mergedCache
};
// Initialize modules
this.mcpWrapper = new MCPClient(mcpClient, this.logger, {
timeout: this.config.timeout,
retryAttempts: this.config.retryAttempts
});
this.cache = new CacheManager(
{ ...mergedCache, cacheDuration: this.config.cacheDuration },
this.logger
);
this.transformer = new TaskTransformer(this.logger);
// Start background refresh if enabled
if (this.config.cache?.enableBackgroundRefresh) {
this.startBackgroundRefresh();
}
this.logger.log('TaskMasterApi: Initialized with modular architecture');
}
/**
* Get tasks from TaskMaster
*/
async getTasks(
options?: GetTasksOptions
): Promise<TaskMasterApiResponse<TaskMasterTask[]>> {
const startTime = Date.now();
const cacheKey = `get_tasks_${JSON.stringify(options || {})}`;
try {
// Check cache first
const cached = this.cache.get(cacheKey);
if (cached) {
return {
success: true,
data: cached,
requestDuration: Date.now() - startTime
};
}
// Prepare MCP tool arguments
const mcpArgs: Record<string, unknown> = {
projectRoot: options?.projectRoot || this.getWorkspaceRoot(),
withSubtasks: options?.withSubtasks ?? true
};
if (options?.status) {
mcpArgs.status = options.status;
}
if (options?.tag) {
mcpArgs.tag = options.tag;
}
this.logger.log('Calling get_tasks with args:', mcpArgs);
// Call MCP tool
const mcpResponse = await this.mcpWrapper.callTool('get_tasks', mcpArgs);
// Transform response
const transformedTasks =
this.transformer.transformMCPTasksResponse(mcpResponse);
// Cache the result
this.cache.set(cacheKey, transformedTasks);
return {
success: true,
data: transformedTasks,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error getting tasks:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update task status
*/
async updateTaskStatus(
taskId: string,
status: string,
options?: UpdateTaskStatusOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
status: status,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
this.logger.log('Calling set_task_status with args:', mcpArgs);
await this.mcpWrapper.callTool('set_task_status', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating task status:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update task content
*/
async updateTask(
taskId: string,
updates: TaskUpdate,
options?: UpdateTaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
// Build update prompt
const updateFields: string[] = [];
if (updates.title !== undefined) {
updateFields.push(`Title: ${updates.title}`);
}
if (updates.description !== undefined) {
updateFields.push(`Description: ${updates.description}`);
}
if (updates.details !== undefined) {
updateFields.push(`Details: ${updates.details}`);
}
if (updates.priority !== undefined) {
updateFields.push(`Priority: ${updates.priority}`);
}
if (updates.testStrategy !== undefined) {
updateFields.push(`Test Strategy: ${updates.testStrategy}`);
}
if (updates.dependencies !== undefined) {
updateFields.push(`Dependencies: ${updates.dependencies.join(', ')}`);
}
const prompt = `Update task with the following changes:\n${updateFields.join('\n')}`;
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
prompt: prompt,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (options?.append !== undefined) {
mcpArgs.append = options.append;
}
if (options?.research !== undefined) {
mcpArgs.research = options.research;
}
this.logger.log('Calling update_task with args:', mcpArgs);
await this.mcpWrapper.callTool('update_task', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update subtask content
*/
async updateSubtask(
taskId: string,
prompt: string,
options?: UpdateSubtaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
prompt: prompt,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (options?.research !== undefined) {
mcpArgs.research = options.research;
}
this.logger.log('Calling update_subtask with args:', mcpArgs);
await this.mcpWrapper.callTool('update_subtask', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating subtask:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Add a new subtask
*/
async addSubtask(
parentTaskId: string,
subtaskData: SubtaskData,
options?: AddSubtaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(parentTaskId),
title: subtaskData.title,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (subtaskData.description) {
mcpArgs.description = subtaskData.description;
}
if (subtaskData.dependencies && subtaskData.dependencies.length > 0) {
mcpArgs.dependencies = subtaskData.dependencies.join(',');
}
if (subtaskData.status) {
mcpArgs.status = subtaskData.status;
}
this.logger.log('Calling add_subtask with args:', mcpArgs);
await this.mcpWrapper.callTool('add_subtask', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error adding subtask:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Get connection status
*/
getConnectionStatus(): { isConnected: boolean; error?: string } {
const status = this.mcpWrapper.getStatus();
return {
isConnected: status.isRunning,
error: status.error
};
}
/**
* Test connection
*/
async testConnection(): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const isConnected = await this.mcpWrapper.testConnection();
return {
success: true,
data: isConnected,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Connection test failed:', error);
return {
success: false,
error:
error instanceof Error ? error.message : 'Connection test failed',
requestDuration: Date.now() - startTime
};
}
}
/**
* Clear all cached data
*/
clearCache(): void {
this.cache.clear();
}
/**
* Get cache analytics
*/
getCacheAnalytics() {
return this.cache.getAnalytics();
}
/**
* Cleanup resources
*/
destroy(): void {
this.cache.destroy();
this.logger.log('TaskMasterApi: Destroyed and cleaned up resources');
}
/**
* Start background refresh
*/
private startBackgroundRefresh(): void {
const interval = this.config.cache?.refreshInterval || 5 * 60 * 1000;
setInterval(() => {
this.performBackgroundRefresh();
}, interval);
}
/**
* Perform background refresh of frequently accessed cache entries
*/
private async performBackgroundRefresh(): Promise<void> {
if (!this.config.cache?.enableBackgroundRefresh) {
return;
}
this.logger.log('Starting background cache refresh');
const candidates = this.cache.getRefreshCandidates();
let refreshedCount = 0;
for (const [key, entry] of candidates) {
try {
const optionsMatch = key.match(/get_tasks_(.+)/);
if (optionsMatch) {
const options = JSON.parse(optionsMatch[1]);
await this.getTasks(options);
refreshedCount++;
this.cache.incrementRefreshes();
}
} catch (error) {
this.logger.warn(`Background refresh failed for key ${key}:`, error);
}
}
this.logger.log(
`Background refresh completed, refreshed ${refreshedCount} entries`
);
}
/**
* Get workspace root path
*/
private getWorkspaceRoot(): string {
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
}
}

View File

@@ -0,0 +1,98 @@
/**
* MCP Client Wrapper
* Handles MCP tool calls with retry logic
*/
import type { ExtensionLogger } from '../logger';
import type { MCPClientManager } from '../mcpClient';
export class MCPClient {
constructor(
private mcpClient: MCPClientManager,
private logger: ExtensionLogger,
private config: { timeout: number; retryAttempts: number }
) {}
/**
* Call MCP tool with retry logic
*/
async callTool(
toolName: string,
args: Record<string, unknown>
): Promise<any> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
try {
const rawResponse = await this.mcpClient.callTool(toolName, args);
this.logger.debug(
`Raw MCP response for ${toolName}:`,
JSON.stringify(rawResponse, null, 2)
);
// Parse MCP response format
if (
rawResponse &&
rawResponse.content &&
Array.isArray(rawResponse.content) &&
rawResponse.content[0]
) {
const contentItem = rawResponse.content[0];
if (contentItem.type === 'text' && contentItem.text) {
try {
const parsedData = JSON.parse(contentItem.text);
this.logger.debug(`Parsed MCP data for ${toolName}:`, parsedData);
return parsedData;
} catch (parseError) {
this.logger.error(
`Failed to parse MCP response text for ${toolName}:`,
parseError
);
this.logger.error(`Raw text was:`, contentItem.text);
return rawResponse; // Fall back to original response
}
}
}
// If not in expected format, return as-is
this.logger.warn(
`Unexpected MCP response format for ${toolName}, returning raw response`
);
return rawResponse;
} catch (error) {
lastError = error instanceof Error ? error : new Error('Unknown error');
this.logger.warn(
`Attempt ${attempt}/${this.config.retryAttempts} failed for ${toolName}:`,
lastError.message
);
if (attempt < this.config.retryAttempts) {
// Exponential backoff
const delay = Math.min(1000 * 2 ** (attempt - 1), 5000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw (
lastError ||
new Error(
`Failed to call ${toolName} after ${this.config.retryAttempts} attempts`
)
);
}
/**
* Get connection status
*/
getStatus(): { isRunning: boolean; error?: string } {
return this.mcpClient.getStatus();
}
/**
* Test connection
*/
async testConnection(): Promise<boolean> {
return this.mcpClient.testConnection();
}
}

View File

@@ -0,0 +1,482 @@
/**
* Task Transformer
* Handles transformation and validation of MCP responses to internal format
*/
import type { ExtensionLogger } from '../../logger';
import { MCPTaskResponse, type TaskMasterTask } from '../types';
export class TaskTransformer {
constructor(private logger: ExtensionLogger) {}
/**
* Transform MCP tasks response to internal format
*/
transformMCPTasksResponse(mcpResponse: any): TaskMasterTask[] {
const transformStartTime = Date.now();
try {
// Validate response structure
const validationResult = this.validateMCPResponse(mcpResponse);
if (!validationResult.isValid) {
this.logger.warn(
'MCP response validation failed:',
validationResult.errors
);
return [];
}
// Handle different response structures
let tasks = [];
if (Array.isArray(mcpResponse)) {
tasks = mcpResponse;
} else if (mcpResponse.data) {
if (Array.isArray(mcpResponse.data)) {
tasks = mcpResponse.data;
} else if (
mcpResponse.data.tasks &&
Array.isArray(mcpResponse.data.tasks)
) {
tasks = mcpResponse.data.tasks;
}
} else if (mcpResponse.tasks && Array.isArray(mcpResponse.tasks)) {
tasks = mcpResponse.tasks;
}
this.logger.log(`Transforming ${tasks.length} tasks from MCP response`, {
responseStructure: {
isArray: Array.isArray(mcpResponse),
hasData: !!mcpResponse.data,
dataIsArray: Array.isArray(mcpResponse.data),
hasDataTasks: !!mcpResponse.data?.tasks,
hasTasks: !!mcpResponse.tasks
}
});
const transformedTasks: TaskMasterTask[] = [];
const transformationErrors: Array<{
taskId: any;
error: string;
task: any;
}> = [];
for (let i = 0; i < tasks.length; i++) {
try {
const task = tasks[i];
const transformedTask = this.transformSingleTask(task, i);
if (transformedTask) {
transformedTasks.push(transformedTask);
}
} catch (error) {
const errorMsg =
error instanceof Error
? error.message
: 'Unknown transformation error';
transformationErrors.push({
taskId: tasks[i]?.id || `unknown_${i}`,
error: errorMsg,
task: tasks[i]
});
this.logger.error(
`Failed to transform task at index ${i}:`,
errorMsg,
tasks[i]
);
}
}
// Log transformation summary
const transformDuration = Date.now() - transformStartTime;
this.logger.log(`Transformation completed in ${transformDuration}ms`, {
totalTasks: tasks.length,
successfulTransformations: transformedTasks.length,
errors: transformationErrors.length,
errorSummary: transformationErrors.map((e) => ({
id: e.taskId,
error: e.error
}))
});
return transformedTasks;
} catch (error) {
this.logger.error(
'Critical error during response transformation:',
error
);
return [];
}
}
/**
* Validate MCP response structure
*/
private validateMCPResponse(mcpResponse: any): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (!mcpResponse) {
errors.push('Response is null or undefined');
return { isValid: false, errors };
}
// Arrays are valid responses
if (Array.isArray(mcpResponse)) {
return { isValid: true, errors };
}
if (typeof mcpResponse !== 'object') {
errors.push('Response is not an object or array');
return { isValid: false, errors };
}
if (mcpResponse.error) {
errors.push(`MCP error: ${mcpResponse.error}`);
}
// Check for valid task structure
const hasValidTasksStructure =
(mcpResponse.data && Array.isArray(mcpResponse.data)) ||
(mcpResponse.data?.tasks && Array.isArray(mcpResponse.data.tasks)) ||
(mcpResponse.tasks && Array.isArray(mcpResponse.tasks));
if (!hasValidTasksStructure && !mcpResponse.error) {
errors.push('Response does not contain a valid tasks array structure');
}
return { isValid: errors.length === 0, errors };
}
/**
* Transform a single task with validation
*/
private transformSingleTask(task: any, index: number): TaskMasterTask | null {
if (!task || typeof task !== 'object') {
this.logger.warn(`Task at index ${index} is not a valid object:`, task);
return null;
}
try {
// Validate required fields
const taskId = this.validateAndNormalizeId(task.id, index);
const title =
this.validateAndNormalizeString(
task.title,
'Untitled Task',
`title for task ${taskId}`
) || 'Untitled Task';
const description =
this.validateAndNormalizeString(
task.description,
'',
`description for task ${taskId}`
) || '';
// Normalize and validate status/priority
const status = this.normalizeStatus(task.status);
const priority = this.normalizePriority(task.priority);
// Handle optional fields
const details = this.validateAndNormalizeString(
task.details,
undefined,
`details for task ${taskId}`
);
const testStrategy = this.validateAndNormalizeString(
task.testStrategy,
undefined,
`testStrategy for task ${taskId}`
);
// Handle complexity score
const complexityScore =
typeof task.complexityScore === 'number'
? task.complexityScore
: undefined;
// Transform dependencies
const dependencies = this.transformDependencies(
task.dependencies,
taskId
);
// Transform subtasks
const subtasks = this.transformSubtasks(task.subtasks, taskId);
const transformedTask: TaskMasterTask = {
id: taskId,
title,
description,
status,
priority,
details,
testStrategy,
complexityScore,
dependencies,
subtasks
};
// Log successful transformation for complex tasks
if (
(subtasks && subtasks.length > 0) ||
dependencies.length > 0 ||
complexityScore !== undefined
) {
this.logger.debug(`Successfully transformed complex task ${taskId}:`, {
subtaskCount: subtasks?.length ?? 0,
dependencyCount: dependencies.length,
status,
priority,
complexityScore
});
}
return transformedTask;
} catch (error) {
this.logger.error(
`Error transforming task at index ${index}:`,
error,
task
);
return null;
}
}
private validateAndNormalizeId(id: any, fallbackIndex: number): string {
if (id === null || id === undefined) {
const generatedId = `generated_${fallbackIndex}_${Date.now()}`;
this.logger.warn(`Task missing ID, generated: ${generatedId}`);
return generatedId;
}
const stringId = String(id).trim();
if (stringId === '') {
const generatedId = `empty_${fallbackIndex}_${Date.now()}`;
this.logger.warn(`Task has empty ID, generated: ${generatedId}`);
return generatedId;
}
return stringId;
}
private validateAndNormalizeString(
value: any,
defaultValue: string | undefined,
fieldName: string
): string | undefined {
if (value === null || value === undefined) {
return defaultValue;
}
if (typeof value !== 'string') {
this.logger.warn(`${fieldName} is not a string, converting:`, value);
return String(value).trim() || defaultValue;
}
const trimmed = value.trim();
if (trimmed === '' && defaultValue !== undefined) {
return defaultValue;
}
return trimmed || defaultValue;
}
private transformDependencies(dependencies: any, taskId: string): string[] {
if (!dependencies) {
return [];
}
if (!Array.isArray(dependencies)) {
this.logger.warn(
`Dependencies for task ${taskId} is not an array:`,
dependencies
);
return [];
}
const validDependencies: string[] = [];
for (let i = 0; i < dependencies.length; i++) {
const dep = dependencies[i];
if (dep === null || dep === undefined) {
this.logger.warn(`Null dependency at index ${i} for task ${taskId}`);
continue;
}
const stringDep = String(dep).trim();
if (stringDep === '') {
this.logger.warn(`Empty dependency at index ${i} for task ${taskId}`);
continue;
}
// Check for self-dependency
if (stringDep === taskId) {
this.logger.warn(
`Self-dependency detected for task ${taskId}, skipping`
);
continue;
}
validDependencies.push(stringDep);
}
return validDependencies;
}
private transformSubtasks(
subtasks: any,
parentTaskId: string
): TaskMasterTask['subtasks'] {
if (!subtasks) {
return [];
}
if (!Array.isArray(subtasks)) {
this.logger.warn(
`Subtasks for task ${parentTaskId} is not an array:`,
subtasks
);
return [];
}
const validSubtasks = [];
for (let i = 0; i < subtasks.length; i++) {
try {
const subtask = subtasks[i];
if (!subtask || typeof subtask !== 'object') {
this.logger.warn(
`Invalid subtask at index ${i} for task ${parentTaskId}:`,
subtask
);
continue;
}
const transformedSubtask = {
id: typeof subtask.id === 'number' ? subtask.id : i + 1,
title:
this.validateAndNormalizeString(
subtask.title,
`Subtask ${i + 1}`,
`subtask title for parent ${parentTaskId}`
) || `Subtask ${i + 1}`,
description: this.validateAndNormalizeString(
subtask.description,
undefined,
`subtask description for parent ${parentTaskId}`
),
status:
this.validateAndNormalizeString(
subtask.status,
'pending',
`subtask status for parent ${parentTaskId}`
) || 'pending',
details: this.validateAndNormalizeString(
subtask.details,
undefined,
`subtask details for parent ${parentTaskId}`
),
testStrategy: this.validateAndNormalizeString(
subtask.testStrategy,
undefined,
`subtask testStrategy for parent ${parentTaskId}`
),
dependencies: subtask.dependencies || []
};
validSubtasks.push(transformedSubtask);
} catch (error) {
this.logger.error(
`Error transforming subtask at index ${i} for task ${parentTaskId}:`,
error
);
}
}
return validSubtasks;
}
private normalizeStatus(status: string): TaskMasterTask['status'] {
const original = status;
const normalized = status?.toLowerCase()?.trim() || 'pending';
const statusMap: Record<string, TaskMasterTask['status']> = {
pending: 'pending',
'in-progress': 'in-progress',
in_progress: 'in-progress',
inprogress: 'in-progress',
progress: 'in-progress',
working: 'in-progress',
active: 'in-progress',
review: 'review',
reviewing: 'review',
'in-review': 'review',
in_review: 'review',
done: 'done',
completed: 'done',
complete: 'done',
finished: 'done',
closed: 'done',
resolved: 'done',
blocked: 'deferred',
block: 'deferred',
stuck: 'deferred',
waiting: 'deferred',
cancelled: 'cancelled',
canceled: 'cancelled',
cancel: 'cancelled',
abandoned: 'cancelled',
deferred: 'deferred',
defer: 'deferred',
postponed: 'deferred',
later: 'deferred'
};
const result = statusMap[normalized] || 'pending';
if (original && original !== result) {
this.logger.debug(`Normalized status '${original}' -> '${result}'`);
}
return result;
}
private normalizePriority(priority: string): TaskMasterTask['priority'] {
const original = priority;
const normalized = priority?.toLowerCase()?.trim() || 'medium';
let result: TaskMasterTask['priority'] = 'medium';
if (
normalized.includes('high') ||
normalized.includes('urgent') ||
normalized.includes('critical') ||
normalized.includes('important') ||
normalized === 'h' ||
normalized === '3'
) {
result = 'high';
} else if (
normalized.includes('low') ||
normalized.includes('minor') ||
normalized.includes('trivial') ||
normalized === 'l' ||
normalized === '1'
) {
result = 'low';
} else if (
normalized.includes('medium') ||
normalized.includes('normal') ||
normalized.includes('standard') ||
normalized === 'm' ||
normalized === '2'
) {
result = 'medium';
}
if (original && original !== result) {
this.logger.debug(`Normalized priority '${original}' -> '${result}'`);
}
return result;
}
}

View File

@@ -0,0 +1,157 @@
/**
* TaskMaster API Types
* All type definitions for the TaskMaster API
*/
// MCP Response Types
export interface MCPTaskResponse {
data?: {
tasks?: Array<{
id: number | string;
title: string;
description: string;
status: string;
priority: string;
details?: string;
testStrategy?: string;
dependencies?: Array<number | string>;
complexityScore?: number;
subtasks?: Array<{
id: number;
title: string;
description?: string;
status: string;
details?: string;
dependencies?: Array<number | string>;
}>;
}>;
tag?: {
currentTag: string;
availableTags: string[];
};
};
version?: {
version: string;
name: string;
};
error?: string;
}
// Internal Task Interface
export interface TaskMasterTask {
id: string;
title: string;
description: string;
status:
| 'pending'
| 'in-progress'
| 'review'
| 'done'
| 'deferred'
| 'cancelled';
priority: 'high' | 'medium' | 'low';
details?: string;
testStrategy?: string;
dependencies?: string[];
complexityScore?: number;
subtasks?: Array<{
id: number;
title: string;
description?: string;
status: string;
details?: string;
testStrategy?: string;
dependencies?: Array<number | string>;
}>;
}
// API Response Wrapper
export interface TaskMasterApiResponse<T = any> {
success: boolean;
data?: T;
error?: string;
requestDuration?: number;
}
// API Configuration
export interface TaskMasterApiConfig {
timeout: number;
retryAttempts: number;
cacheDuration: number;
projectRoot?: string;
cache?: CacheConfig;
}
export interface CacheConfig {
maxSize: number;
enableBackgroundRefresh: boolean;
refreshInterval: number;
enableAnalytics: boolean;
enablePrefetch: boolean;
compressionEnabled: boolean;
persistToDisk: boolean;
}
// Cache Types
export interface CacheEntry {
data: any;
timestamp: number;
accessCount: number;
lastAccessed: number;
size: number;
ttl?: number;
tags: string[];
}
export interface CacheAnalytics {
hits: number;
misses: number;
evictions: number;
refreshes: number;
totalSize: number;
averageAccessTime: number;
hitRate: number;
}
// Method Options
export interface GetTasksOptions {
status?: string;
withSubtasks?: boolean;
tag?: string;
projectRoot?: string;
}
export interface UpdateTaskStatusOptions {
projectRoot?: string;
}
export interface UpdateTaskOptions {
projectRoot?: string;
append?: boolean;
research?: boolean;
}
export interface UpdateSubtaskOptions {
projectRoot?: string;
research?: boolean;
}
export interface AddSubtaskOptions {
projectRoot?: string;
}
export interface TaskUpdate {
title?: string;
description?: string;
details?: string;
priority?: 'high' | 'medium' | 'low';
testStrategy?: string;
dependencies?: string[];
}
export interface SubtaskData {
title: string;
description?: string;
dependencies?: string[];
status?: string;
}

View File

@@ -0,0 +1,106 @@
/**
* Main App Component
*/
import React, { useReducer, useState, useEffect, useRef } from 'react';
import { VSCodeContext } from './contexts/VSCodeContext';
import { QueryProvider } from './providers/QueryProvider';
import { AppContent } from './components/AppContent';
import { ToastContainer } from './components/ToastContainer';
import { ErrorBoundary } from './components/ErrorBoundary';
import { appReducer, initialState } from './reducers/appReducer';
import { useWebviewHeight } from './hooks/useWebviewHeight';
import { useVSCodeMessages } from './hooks/useVSCodeMessages';
import {
showSuccessToast,
showInfoToast,
showWarningToast,
showErrorToast,
createToast
} from './utils/toast';
export const App: React.FC = () => {
const [state, dispatch] = useReducer(appReducer, initialState);
const [vscode] = useState(() => window.acquireVsCodeApi?.());
const availableHeight = useWebviewHeight();
const { sendMessage } = useVSCodeMessages(vscode, state, dispatch);
const hasInitialized = useRef(false);
// Initialize the webview
useEffect(() => {
if (hasInitialized.current) return;
hasInitialized.current = true;
if (!vscode) {
console.warn('⚠️ VS Code API not available - running in standalone mode');
dispatch({
type: 'SET_CONNECTION_STATUS',
payload: { isConnected: false, status: 'Standalone Mode' }
});
return;
}
console.log('🔄 Initializing webview...');
// Notify extension that webview is ready
vscode.postMessage({ type: 'ready' });
// React Query will handle task fetching, so we only need to load tags data
sendMessage({ type: 'getTags' })
.then((tagsData) => {
if (tagsData?.tags && tagsData?.currentTag) {
const tagNames = tagsData.tags.map((tag: any) => tag.name || tag);
dispatch({
type: 'SET_TAG_DATA',
payload: {
currentTag: tagsData.currentTag,
availableTags: tagNames
}
});
}
})
.catch((error) => {
console.error('❌ Failed to load tags:', error);
});
}, [vscode, sendMessage, dispatch]);
const contextValue = {
vscode,
state,
dispatch,
sendMessage,
availableHeight,
// Toast notification functions
showSuccessToast: showSuccessToast(dispatch),
showInfoToast: showInfoToast(dispatch),
showWarningToast: showWarningToast(dispatch),
showErrorToast: showErrorToast(dispatch)
};
return (
<QueryProvider>
<VSCodeContext.Provider value={contextValue}>
<ErrorBoundary
onError={(error) => {
// Handle React errors and show appropriate toast
dispatch({
type: 'ADD_TOAST',
payload: createToast(
'error',
'Component Error',
`A React component crashed: ${error.message}`,
10000
)
});
}}
>
<AppContent />
<ToastContainer
notifications={state.toastNotifications}
onDismiss={(id) => dispatch({ type: 'REMOVE_TOAST', payload: id })}
/>
</ErrorBoundary>
</VSCodeContext.Provider>
</QueryProvider>
);
};

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { TaskMasterKanban } from './TaskMasterKanban';
import TaskDetailsView from '@/components/TaskDetailsView';
import { ConfigView } from '@/components/ConfigView';
import { useVSCodeContext } from '../contexts/VSCodeContext';
export const AppContent: React.FC = () => {
const { state, dispatch, sendMessage } = useVSCodeContext();
if (state.currentView === 'config') {
return (
<ConfigView
sendMessage={sendMessage}
onNavigateBack={() => dispatch({ type: 'NAVIGATE_TO_KANBAN' })}
/>
);
}
if (state.currentView === 'task-details' && state.selectedTaskId) {
return (
<TaskDetailsView
taskId={state.selectedTaskId}
onNavigateBack={() => dispatch({ type: 'NAVIGATE_TO_KANBAN' })}
onNavigateToTask={(taskId: string) =>
dispatch({ type: 'NAVIGATE_TO_TASK', payload: taskId })
}
/>
);
}
// Default to Kanban view
return <TaskMasterKanban />;
};

View File

@@ -0,0 +1,138 @@
import React from 'react';
import { ExternalLink, Terminal, MessageSquare, Plus } from 'lucide-react';
import { TaskMasterLogo } from '../../components/TaskMasterLogo';
interface EmptyStateProps {
currentTag: string;
}
export const EmptyState: React.FC<EmptyStateProps> = ({ currentTag }) => {
return (
<div className="flex items-center justify-center h-full overflow-auto">
<div className="max-w-2xl mx-auto text-center p-8">
{/* Empty state illustration */}
<div className="mb-8 max-w-96 mx-auto">
<TaskMasterLogo className="w-32 h-32 mx-auto text-vscode-foreground/20" />
</div>
<h2 className="text-2xl font-semibold mb-2 text-vscode-foreground">
No tasks in "{currentTag}" tag
</h2>
<p className="text-vscode-foreground/70 mb-8">
Get started by adding tasks to this tag using the commands below
</p>
{/* Command suggestions */}
<div className="space-y-4 text-left">
<div className="bg-vscode-editor-background/50 border border-vscode-panel-border rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<Terminal className="w-4 h-4 text-vscode-terminal-ansiGreen" />
<h3 className="font-medium">CLI Commands</h3>
</div>
<div className="space-y-2">
<div className="bg-vscode-editor-background rounded p-2 font-mono text-sm">
<span className="text-vscode-terminal-ansiYellow">
task-master
</span>{' '}
<span className="text-vscode-terminal-ansiCyan">parse-prd</span>{' '}
<span className="text-vscode-foreground/70">
&lt;path-to-prd&gt;
</span>{' '}
<span className="text-vscode-terminal-ansiMagenta">
--append
</span>
<div className="text-xs text-vscode-foreground/50 mt-1">
Parse a PRD and append tasks to current tag
</div>
</div>
<div className="bg-vscode-editor-background rounded p-2 font-mono text-sm">
<span className="text-vscode-terminal-ansiYellow">
task-master
</span>{' '}
<span className="text-vscode-terminal-ansiCyan">add-task</span>{' '}
<span className="text-vscode-terminal-ansiMagenta">
--prompt
</span>{' '}
<span className="text-vscode-foreground/70">
"Your task description"
</span>
<div className="text-xs text-vscode-foreground/50 mt-1">
Add a single task with AI assistance
</div>
</div>
<div className="bg-vscode-editor-background rounded p-2 font-mono text-sm">
<span className="text-vscode-terminal-ansiYellow">
task-master
</span>{' '}
<span className="text-vscode-terminal-ansiCyan">add-task</span>{' '}
<span className="text-vscode-terminal-ansiMagenta">--help</span>
<div className="text-xs text-vscode-foreground/50 mt-1">
View all options for adding tasks
</div>
</div>
</div>
</div>
<div className="bg-vscode-editor-background/50 border border-vscode-panel-border rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<MessageSquare className="w-4 h-4 text-vscode-textLink-foreground" />
<h3 className="font-medium">MCP Examples</h3>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-start gap-2">
<Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" />
<div>
<div className="text-vscode-foreground">
"Add a task to tag {currentTag}: Implement user
authentication"
</div>
</div>
</div>
<div className="flex items-start gap-2">
<Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" />
<div>
<div className="text-vscode-foreground">
"Parse this PRD and add tasks to {currentTag}: [paste PRD
content]"
</div>
</div>
</div>
<div className="flex items-start gap-2">
<Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" />
<div>
<div className="text-vscode-foreground">
"Create 5 tasks for building a REST API in tag {currentTag}"
</div>
</div>
</div>
</div>
</div>
{/* Documentation link */}
<div className="flex justify-center pt-4">
<a
href="https://docs.task-master.dev"
className="inline-flex items-center gap-2 text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground transition-colors"
onClick={(e) => {
e.preventDefault();
// Use VS Code API to open external link
if (window.acquireVsCodeApi) {
const vscode = window.acquireVsCodeApi();
vscode.postMessage({
type: 'openExternal',
url: 'https://docs.task-master.dev'
});
}
}}
>
<ExternalLink className="w-4 h-4" />
<span className="text-sm font-medium">
View TaskMaster Documentation
</span>
</a>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,113 @@
/**
* Error Boundary Component
*/
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
errorInfo?: React.ErrorInfo;
}
interface ErrorBoundaryProps {
children: React.ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('React Error Boundary caught:', error, errorInfo);
// Log to extension
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// Send error to extension for centralized handling
if (window.acquireVsCodeApi) {
const vscode = window.acquireVsCodeApi();
vscode.postMessage({
type: 'reactError',
data: {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: Date.now()
}
});
}
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-vscode-background">
<div className="max-w-md mx-auto text-center p-6">
<div className="w-16 h-16 mx-auto mb-4 text-red-400">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.962-.833-2.732 0L3.732 19c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<h2 className="text-xl font-semibold text-vscode-foreground mb-2">
Something went wrong
</h2>
<p className="text-vscode-foreground/70 mb-4">
The Task Master Kanban board encountered an unexpected error.
</p>
<div className="space-y-2">
<button
onClick={() =>
this.setState({
hasError: false,
error: undefined,
errorInfo: undefined
})
}
className="w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors"
>
Try Again
</button>
<button
onClick={() => window.location.reload()}
className="w-full px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-md transition-colors"
>
Reload Extension
</button>
</div>
{this.state.error && (
<details className="mt-4 text-left">
<summary className="text-sm text-vscode-foreground/50 cursor-pointer">
Error Details
</summary>
<pre className="mt-2 text-xs text-vscode-foreground/70 bg-vscode-input/30 p-2 rounded overflow-auto max-h-32">
{this.state.error.message}
{this.state.error.stack && `\n\n${this.state.error.stack}`}
</pre>
</details>
)}
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,84 @@
/**
* Polling Status Indicator Component
*/
import React from 'react';
import type { AppState } from '../types';
interface PollingStatusProps {
polling: AppState['polling'];
onRetry?: () => void;
}
export const PollingStatus: React.FC<PollingStatusProps> = ({
polling,
onRetry
}) => {
const {
isActive,
errorCount,
isOfflineMode,
connectionStatus,
reconnectAttempts,
maxReconnectAttempts
} = polling;
if (isOfflineMode || connectionStatus === 'offline') {
return (
<div className="flex items-center gap-2">
<div
className="flex items-center gap-1 text-red-400"
title="Offline mode - using cached data"
>
<div className="w-2 h-2 rounded-full bg-red-400" />
<span className="text-xs">Offline</span>
</div>
<button
onClick={onRetry}
className="text-xs text-blue-400 hover:underline"
title="Attempt to reconnect"
>
Retry
</button>
</div>
);
}
if (connectionStatus === 'reconnecting') {
return (
<div
className="flex items-center gap-1 text-yellow-400"
title={`Reconnecting... (${reconnectAttempts}/${maxReconnectAttempts})`}
>
<div className="w-2 h-2 rounded-full bg-yellow-400 animate-pulse" />
<span className="text-xs">Reconnecting</span>
</div>
);
}
if (errorCount > 0) {
return (
<div
className="flex items-center gap-1 text-yellow-400"
title={`${errorCount} polling error${errorCount > 1 ? 's' : ''}`}
>
<div className="w-2 h-2 rounded-full bg-yellow-400" />
<span className="text-xs">Live (errors)</span>
</div>
);
}
if (isActive) {
return (
<div
className="flex items-center gap-1 text-green-400"
title="Live updates active"
>
<div className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
<span className="text-xs">Live</span>
</div>
);
}
return null;
};

View File

@@ -0,0 +1,30 @@
/**
* Priority Badge Component
*/
import React from 'react';
import { Badge } from '@/components/ui/badge';
import type { TaskMasterTask } from '../types';
interface PriorityBadgeProps {
priority: TaskMasterTask['priority'];
}
export const PriorityBadge: React.FC<PriorityBadgeProps> = ({ priority }) => {
if (!priority) return null;
const variants = {
high: 'destructive' as const,
medium: 'default' as const,
low: 'secondary' as const
};
return (
<Badge
variant={variants[priority] || 'secondary'}
className="text-xs font-normal px-2 py-0.5"
>
{priority}
</Badge>
);
};

View File

@@ -0,0 +1,52 @@
import React, { useState, useEffect } from 'react';
import { TaskMasterLogo } from '../../components/TaskMasterLogo';
interface SidebarViewProps {
initialConnectionStatus?: boolean;
}
// Acquire VS Code API only once globally to avoid "already acquired" error
const vscode = window.acquireVsCodeApi ? window.acquireVsCodeApi() : null;
export const SidebarView: React.FC<SidebarViewProps> = ({
initialConnectionStatus = false
}) => {
const [isConnected, setIsConnected] = useState(initialConnectionStatus);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data;
if (message.type === 'connectionStatus') {
setIsConnected(message.data.isConnected);
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);
const handleOpenBoard = () => {
vscode?.postMessage({ command: 'openBoard' });
};
return (
<div className="h-full flex items-center justify-center p-6">
<div className="text-center">
<TaskMasterLogo className="w-20 h-20 mx-auto mb-5 opacity-80 text-vscode-foreground" />
<h2 className="text-xl font-semibold mb-6 text-vscode-foreground">
TaskMaster
</h2>
<button
onClick={handleOpenBoard}
className="w-full px-4 py-2 bg-vscode-button-background text-vscode-button-foreground rounded hover:bg-vscode-button-hoverBackground transition-colors text-sm font-medium"
>
Open Kanban Board
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,141 @@
import React, { useState, useEffect, useRef } from 'react';
interface TagDropdownProps {
currentTag: string;
availableTags: string[];
onTagSwitch: (tagName: string) => Promise<void>;
sendMessage: (message: any) => Promise<any>;
dispatch: React.Dispatch<any>;
}
export const TagDropdown: React.FC<TagDropdownProps> = ({
currentTag,
availableTags,
onTagSwitch,
sendMessage,
dispatch
}) => {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Fetch tags when component mounts
useEffect(() => {
fetchTags();
}, []);
// Handle click outside to close dropdown
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [isOpen]);
const fetchTags = async () => {
try {
const result = await sendMessage({ type: 'getTags' });
if (result?.tags && result?.currentTag) {
const tagNames = result.tags.map((tag: any) => tag.name || tag);
dispatch({
type: 'SET_TAG_DATA',
payload: {
currentTag: result.currentTag,
availableTags: tagNames
}
});
}
} catch (error) {
console.error('Failed to fetch tags:', error);
}
};
const handleTagSwitch = async (tagName: string) => {
if (tagName === currentTag) {
setIsOpen(false);
return;
}
setIsLoading(true);
try {
await onTagSwitch(tagName);
dispatch({ type: 'SET_CURRENT_TAG', payload: tagName });
setIsOpen(false);
} catch (error) {
console.error('Failed to switch tag:', error);
} finally {
setIsLoading(false);
}
};
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
disabled={isLoading}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border rounded hover:bg-vscode-list-hoverBackground transition-colors"
>
<span className="font-medium">{currentTag}</span>
<svg
className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isOpen && (
<div className="absolute top-full mt-1 right-0 bg-background border border-vscode-dropdown-border rounded shadow-lg z-50 min-w-[200px] py-1">
{availableTags.map((tag) => (
<button
key={tag}
onClick={() => handleTagSwitch(tag)}
className={`w-full text-left px-3 py-2 text-sm transition-colors flex items-center justify-between group
${
tag === currentTag
? 'bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground'
: 'hover:bg-vscode-list-hoverBackground text-vscode-dropdown-foreground'
}`}
>
<span className="truncate pr-2">{tag}</span>
{tag === currentTag && (
<svg
className="w-4 h-4 flex-shrink-0 text-vscode-textLink-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
))}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,80 @@
/**
* Task Card Component for Kanban Board
*/
import React from 'react';
import { KanbanCard } from '@/components/ui/shadcn-io/kanban';
import { PriorityBadge } from './PriorityBadge';
import type { TaskMasterTask } from '../types';
interface TaskCardProps {
task: TaskMasterTask;
dragging?: boolean;
onViewDetails?: (taskId: string) => void;
}
export const TaskCard: React.FC<TaskCardProps> = ({
task,
dragging,
onViewDetails
}) => {
const handleCardClick = (e: React.MouseEvent) => {
e.preventDefault();
onViewDetails?.(task.id);
};
return (
<KanbanCard
id={task.id}
name={task.title}
index={0} // Index is not used in our implementation
parent={task.status}
className="cursor-pointer p-3 transition-shadow hover:shadow-md bg-vscode-editor-background border-vscode-border group"
onClick={handleCardClick}
>
<div className="space-y-3 h-full flex flex-col">
<div className="flex items-start justify-between gap-2 flex-shrink-0">
<h3 className="font-medium text-sm leading-tight flex-1 min-w-0 text-vscode-foreground">
{task.title}
</h3>
<div className="flex items-center gap-1 flex-shrink-0">
<PriorityBadge priority={task.priority} />
</div>
</div>
{task.description && (
<p className="text-xs text-vscode-foreground/70 line-clamp-3 leading-relaxed flex-1 min-h-0">
{task.description}
</p>
)}
<div className="flex items-center justify-between text-xs mt-auto pt-2 flex-shrink-0 border-t border-vscode-border/20">
<span className="font-mono text-vscode-foreground/50 flex-shrink-0">
#{task.id}
</span>
{task.dependencies && task.dependencies.length > 0 && (
<div className="flex items-center gap-1 text-vscode-foreground/50 flex-shrink-0 ml-2">
<span>Deps:</span>
<div className="flex items-center gap-1">
{task.dependencies.map((depId, index) => (
<React.Fragment key={depId}>
<button
className="font-mono hover:text-vscode-link-activeForeground hover:underline transition-colors"
onClick={(e) => {
e.stopPropagation();
onViewDetails?.(depId);
}}
>
#{depId}
</button>
{index < task.dependencies!.length - 1 && <span>,</span>}
</React.Fragment>
))}
</div>
</div>
)}
</div>
</div>
</KanbanCard>
);
};

View File

@@ -0,0 +1,242 @@
/**
* Task Edit Modal Component
*/
import React, { useState, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import type { TaskMasterTask, TaskUpdates } from '../types';
interface TaskEditModalProps {
task: TaskMasterTask;
onSave: (taskId: string, updates: TaskUpdates) => Promise<void>;
onCancel: () => void;
}
export const TaskEditModal: React.FC<TaskEditModalProps> = ({
task,
onSave,
onCancel
}) => {
const [updates, setUpdates] = useState<TaskUpdates>({
title: task.title,
description: task.description || '',
details: task.details || '',
testStrategy: task.testStrategy || '',
priority: task.priority,
dependencies: task.dependencies || []
});
const [isSaving, setIsSaving] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const titleInputRef = useRef<HTMLInputElement>(null);
// Focus title input on mount
useEffect(() => {
titleInputRef.current?.focus();
titleInputRef.current?.select();
}, []);
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
setIsSaving(true);
try {
await onSave(task.id, updates);
} catch (error) {
console.error('Failed to save task:', error);
} finally {
setIsSaving(false);
}
};
const hasChanges = () => {
return (
updates.title !== task.title ||
updates.description !== (task.description || '') ||
updates.details !== (task.details || '') ||
updates.testStrategy !== (task.testStrategy || '') ||
updates.priority !== task.priority ||
JSON.stringify(updates.dependencies) !==
JSON.stringify(task.dependencies || [])
);
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-vscode-editor-background border border-vscode-border rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-vscode-border">
<h2 className="text-lg font-semibold">Edit Task #{task.id}</h2>
<button
onClick={onCancel}
className="text-vscode-foreground/50 hover:text-vscode-foreground transition-colors"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Form */}
<form
ref={formRef}
onSubmit={handleSubmit}
className="flex-1 overflow-y-auto p-4 space-y-4"
>
{/* Title */}
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<input
ref={titleInputRef}
id="title"
type="text"
value={updates.title || ''}
onChange={(e) =>
setUpdates({ ...updates, title: e.target.value })
}
className="w-full px-3 py-2 bg-vscode-input border border-vscode-border rounded-md text-vscode-foreground focus:outline-none focus:ring-2 focus:ring-vscode-focusBorder"
placeholder="Task title"
/>
</div>
{/* Priority */}
<div className="space-y-2">
<Label htmlFor="priority">Priority</Label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-between">
<span className="capitalize">{updates.priority}</span>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-full">
<DropdownMenuItem
onClick={() => setUpdates({ ...updates, priority: 'high' })}
>
High
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setUpdates({ ...updates, priority: 'medium' })}
>
Medium
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setUpdates({ ...updates, priority: 'low' })}
>
Low
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={updates.description || ''}
onChange={(e) =>
setUpdates({ ...updates, description: e.target.value })
}
className="min-h-[80px]"
placeholder="Brief description of the task"
/>
</div>
{/* Details */}
<div className="space-y-2">
<Label htmlFor="details">Implementation Details</Label>
<Textarea
id="details"
value={updates.details || ''}
onChange={(e) =>
setUpdates({ ...updates, details: e.target.value })
}
className="min-h-[120px]"
placeholder="Technical details and implementation notes"
/>
</div>
{/* Test Strategy */}
<div className="space-y-2">
<Label htmlFor="testStrategy">Test Strategy</Label>
<Textarea
id="testStrategy"
value={updates.testStrategy || ''}
onChange={(e) =>
setUpdates({ ...updates, testStrategy: e.target.value })
}
className="min-h-[80px]"
placeholder="How to test this task"
/>
</div>
{/* Dependencies */}
<div className="space-y-2">
<Label htmlFor="dependencies">
Dependencies (comma-separated task IDs)
</Label>
<input
id="dependencies"
type="text"
value={updates.dependencies?.join(', ') || ''}
onChange={(e) =>
setUpdates({
...updates,
dependencies: e.target.value
.split(',')
.map((d) => d.trim())
.filter(Boolean)
})
}
className="w-full px-3 py-2 bg-vscode-input border border-vscode-border rounded-md text-vscode-foreground focus:outline-none focus:ring-2 focus:ring-vscode-focusBorder"
placeholder="e.g., 1, 2.1, 3"
/>
</div>
</form>
{/* Footer */}
<div className="flex items-center justify-end gap-2 p-4 border-t border-vscode-border">
<Button variant="outline" onClick={onCancel} disabled={isSaving}>
Cancel
</Button>
<Button
onClick={() => handleSubmit()}
disabled={isSaving || !hasChanges()}
>
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,412 @@
/**
* Main Kanban Board Component
*/
import React, { useState, useCallback, useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { RefreshCw } from 'lucide-react';
import {
type DragEndEvent,
KanbanBoard,
KanbanCards,
KanbanHeader,
KanbanProvider
} from '@/components/ui/shadcn-io/kanban';
import { TaskCard } from './TaskCard';
import { TaskEditModal } from './TaskEditModal';
import { PollingStatus } from './PollingStatus';
import { TagDropdown } from './TagDropdown';
import { EmptyState } from './EmptyState';
import { useVSCodeContext } from '../contexts/VSCodeContext';
import {
useTasks,
useUpdateTaskStatus,
useUpdateTask,
taskKeys
} from '../hooks/useTaskQueries';
import { kanbanStatuses, HEADER_HEIGHT } from '../constants';
import type { TaskMasterTask, TaskUpdates } from '../types';
export const TaskMasterKanban: React.FC = () => {
const { state, dispatch, sendMessage, availableHeight } = useVSCodeContext();
const queryClient = useQueryClient();
const {
error: legacyError,
editingTask,
polling,
currentTag,
availableTags
} = state;
const [activeTask, setActiveTask] = useState<TaskMasterTask | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
// Use React Query to fetch tasks
const {
data: serverTasks = [],
isLoading,
error,
isFetching,
isSuccess
} = useTasks({ tag: currentTag });
const updateTaskStatus = useUpdateTaskStatus();
const updateTask = useUpdateTask();
// Debug logging
console.log('🔍 TaskMasterKanban Query State:', {
isLoading,
isFetching,
isSuccess,
tasksCount: serverTasks?.length,
error
});
// Temporary state only for active drag operations
const [tempReorderedTasks, setTempReorderedTasks] = useState<
TaskMasterTask[] | null
>(null);
// Use temp tasks only if actively set, otherwise use server tasks
const tasks = tempReorderedTasks ?? serverTasks;
// Calculate header height for proper kanban board sizing
const kanbanHeight = availableHeight - HEADER_HEIGHT;
// Group tasks by status
const tasksByStatus = kanbanStatuses.reduce(
(acc, status) => {
acc[status.id] = tasks.filter((task) => task.status === status.id);
return acc;
},
{} as Record<string, TaskMasterTask[]>
);
// Debug logging
console.log('TaskMasterKanban render:', {
tasksCount: tasks.length,
currentTag,
tasksByStatus: Object.entries(tasksByStatus).map(([status, tasks]) => ({
status,
count: tasks.length,
taskIds: tasks.map((t) => t.id)
})),
allTaskIds: tasks.map((t) => ({ id: t.id, title: t.title }))
});
// Handle task update
const handleUpdateTask = async (taskId: string, updates: TaskUpdates) => {
console.log(`🔄 Updating task ${taskId} content:`, updates);
try {
await updateTask.mutateAsync({
taskId,
updates,
options: { append: false, research: false }
});
console.log(`✅ Task ${taskId} content updated successfully`);
// Close the edit modal
dispatch({
type: 'SET_EDITING_TASK',
payload: { taskId: null }
});
} catch (error) {
console.error(`❌ Failed to update task ${taskId}:`, error);
dispatch({
type: 'SET_ERROR',
payload: `Failed to update task: ${error}`
});
}
};
// Handle drag start
const handleDragStart = useCallback(
(event: DragEndEvent) => {
const taskId = event.active.id as string;
const task = tasks.find((t) => t.id === taskId);
if (task) {
setActiveTask(task);
}
},
[tasks]
);
// Handle drag cancel
const handleDragCancel = useCallback(() => {
setActiveTask(null);
// Clear any temporary state
setTempReorderedTasks(null);
}, []);
// Handle drag end
const handleDragEnd = useCallback(
async (event: DragEndEvent) => {
const { active, over } = event;
// Reset active task
setActiveTask(null);
if (!over || active.id === over.id) {
// Clear any temp state if drag was cancelled
setTempReorderedTasks(null);
return;
}
const taskId = active.id as string;
const newStatus = over.id as TaskMasterTask['status'];
// Find the task
const task = tasks.find((t) => t.id === taskId);
if (!task || task.status === newStatus) {
// Clear temp state if no change needed
setTempReorderedTasks(null);
return;
}
// Create the optimistically reordered tasks
const reorderedTasks = tasks.map((t) =>
t.id === taskId ? { ...t, status: newStatus } : t
);
// Set temporary state to show immediate visual feedback
setTempReorderedTasks(reorderedTasks);
try {
// Update on server - React Query will handle optimistic updates
await updateTaskStatus.mutateAsync({ taskId, newStatus });
// Clear temp state after mutation starts successfully
setTempReorderedTasks(null);
} catch (error) {
// On error, clear temp state - React Query will revert optimistic update
setTempReorderedTasks(null);
dispatch({
type: 'SET_ERROR',
payload: `Failed to update task status: ${error}`
});
}
},
[tasks, updateTaskStatus, dispatch]
);
// Handle retry connection
const handleRetry = useCallback(() => {
sendMessage({ type: 'retryConnection' });
}, [sendMessage]);
// Handle refresh
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
try {
// Invalidate all task queries
await queryClient.invalidateQueries({ queryKey: taskKeys.all });
} finally {
// Reset after a short delay to show the animation
setTimeout(() => setIsRefreshing(false), 500);
}
}, [queryClient]);
// Handle tag switching
const handleTagSwitch = useCallback(
async (tagName: string) => {
console.log('Switching to tag:', tagName);
await sendMessage({ type: 'switchTag', data: { tagName } });
dispatch({
type: 'SET_TAG_DATA',
payload: { currentTag: tagName, availableTags }
});
},
[sendMessage, dispatch, availableTags]
);
// Use React Query loading state
const displayError = error
? error instanceof Error
? error.message
: String(error)
: legacyError;
if (isLoading) {
return (
<div
className="flex items-center justify-center"
style={{ height: `${kanbanHeight}px` }}
>
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-vscode-foreground mx-auto mb-4" />
<p className="text-sm text-vscode-foreground/70">Loading tasks...</p>
</div>
</div>
);
}
if (displayError) {
return (
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 m-4">
<p className="text-red-400 text-sm">Error: {displayError}</p>
<button
onClick={() => dispatch({ type: 'CLEAR_ERROR' })}
className="mt-2 text-sm text-red-400 hover:text-red-300 underline"
>
Dismiss
</button>
</div>
);
}
return (
<>
<div className="flex flex-col" style={{ height: `${availableHeight}px` }}>
<div className="flex-shrink-0 p-4 bg-vscode-sidebar-background border-b border-vscode-border">
<div className="flex items-center justify-between">
<h1 className="text-lg font-semibold text-vscode-foreground">
TaskMaster Kanban
</h1>
<div className="flex items-center gap-4">
<TagDropdown
currentTag={currentTag}
availableTags={availableTags}
onTagSwitch={handleTagSwitch}
sendMessage={sendMessage}
dispatch={dispatch}
/>
<button
onClick={handleRefresh}
disabled={isRefreshing}
className="p-1.5 rounded hover:bg-vscode-button-hoverBackground transition-colors"
title="Refresh tasks"
>
<RefreshCw
className={`w-4 h-4 text-vscode-foreground/70 ${isRefreshing ? 'animate-spin' : ''}`}
/>
</button>
<PollingStatus polling={polling} onRetry={handleRetry} />
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${state.isConnected ? 'bg-green-400' : 'bg-red-400'}`}
/>
<span className="text-xs text-vscode-foreground/70">
{state.connectionStatus}
</span>
</div>
<button
onClick={() => dispatch({ type: 'NAVIGATE_TO_CONFIG' })}
className="p-1.5 rounded hover:bg-vscode-button-hoverBackground transition-colors"
title="TaskMaster Configuration"
>
<svg
className="w-4 h-4 text-vscode-foreground/70"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
</div>
</div>
</div>
<div
className="flex-1 px-4 py-4 overflow-hidden"
style={{ height: `${kanbanHeight}px` }}
>
{tasks.length === 0 ? (
<EmptyState currentTag={currentTag} />
) : (
<KanbanProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
className="kanban-container w-full h-full overflow-x-auto overflow-y-hidden"
dragOverlay={
activeTask ? <TaskCard task={activeTask} dragging /> : null
}
>
<div className="flex gap-4 h-full min-w-fit">
{kanbanStatuses.map((status) => {
const statusTasks = tasksByStatus[status.id] || [];
const hasScrollbar = statusTasks.length > 4;
return (
<KanbanBoard
key={status.id}
id={status.id}
className={`
w-80 flex flex-col
border border-vscode-border/30
rounded-lg
bg-vscode-sidebar-background/50
`}
>
<KanbanHeader
name={`${status.title} (${statusTasks.length})`}
color={status.color}
className="px-3 py-3 text-sm font-medium flex-shrink-0 border-b border-vscode-border/30"
/>
<div
className={`
flex flex-col gap-2
overflow-y-auto overflow-x-hidden
p-2
scrollbar-thin scrollbar-track-transparent
${hasScrollbar ? 'pr-1' : ''}
`}
style={{
maxHeight: `${kanbanHeight - 80}px`
}}
>
<KanbanCards>
{statusTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onViewDetails={(taskId) => {
console.log(
'🔍 Navigating to task details:',
taskId
);
dispatch({
type: 'NAVIGATE_TO_TASK',
payload: taskId
});
}}
/>
))}
</KanbanCards>
</div>
</KanbanBoard>
);
})}
</div>
</KanbanProvider>
)}
</div>
</div>
{/* Task Edit Modal */}
{editingTask?.taskId && editingTask.editData && (
<TaskEditModal
task={editingTask.editData}
onSave={handleUpdateTask}
onCancel={() => {
dispatch({
type: 'SET_EDITING_TASK',
payload: { taskId: null }
});
}}
/>
)}
</>
);
};

View File

@@ -0,0 +1,31 @@
/**
* Toast Container Component
*/
import React from 'react';
import { ToastNotification } from './ToastNotification';
import type { ToastNotification as ToastType } from '../types';
interface ToastContainerProps {
notifications: ToastType[];
onDismiss: (id: string) => void;
}
export const ToastContainer: React.FC<ToastContainerProps> = ({
notifications,
onDismiss
}) => {
return (
<div className="fixed top-4 right-4 z-50 pointer-events-none">
<div className="flex flex-col items-end pointer-events-auto">
{notifications.map((notification) => (
<ToastNotification
key={notification.id}
notification={notification}
onDismiss={onDismiss}
/>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,167 @@
/**
* Toast Notification Component
*/
import React, { useState, useEffect } from 'react';
import type { ToastNotification as ToastType } from '../types';
interface ToastNotificationProps {
notification: ToastType;
onDismiss: (id: string) => void;
}
export const ToastNotification: React.FC<ToastNotificationProps> = ({
notification,
onDismiss
}) => {
const [isVisible, setIsVisible] = useState(true);
const [progress, setProgress] = useState(100);
const duration = notification.duration || 5000; // 5 seconds default
useEffect(() => {
const progressInterval = setInterval(() => {
setProgress((prev) => {
const decrease = (100 / duration) * 100; // Update every 100ms
return Math.max(0, prev - decrease);
});
}, 100);
const timeoutId = setTimeout(() => {
setIsVisible(false);
setTimeout(() => onDismiss(notification.id), 300); // Wait for animation
}, duration);
return () => {
clearInterval(progressInterval);
clearTimeout(timeoutId);
};
}, [notification.id, duration, onDismiss]);
const getIcon = () => {
switch (notification.type) {
case 'success':
return (
<svg
className="w-5 h-5 text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
);
case 'info':
return (
<svg
className="w-5 h-5 text-blue-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
case 'warning':
return (
<svg
className="w-5 h-5 text-yellow-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.667-2.308-1.667-3.08 0L3.34 19c-.77 1.333.192 3 1.732 3z"
/>
</svg>
);
case 'error':
return (
<svg
className="w-5 h-5 text-red-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
}
};
const bgColor = {
success: 'bg-green-900/90',
info: 'bg-blue-900/90',
warning: 'bg-yellow-900/90',
error: 'bg-red-900/90'
}[notification.type];
const borderColor = {
success: 'border-green-600',
info: 'border-blue-600',
warning: 'border-yellow-600',
error: 'border-red-600'
}[notification.type];
const progressColor = {
success: 'bg-green-400',
info: 'bg-blue-400',
warning: 'bg-yellow-400',
error: 'bg-red-400'
}[notification.type];
return (
<div
className={`${bgColor} ${borderColor} border rounded-lg shadow-lg p-4 mb-2 transition-all duration-300 ${
isVisible ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full'
} max-w-sm w-full relative overflow-hidden`}
>
<div className="flex items-start">
<div className="flex-shrink-0">{getIcon()}</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-white">
{notification.title}
</h3>
<p className="mt-1 text-sm text-gray-300">{notification.message}</p>
</div>
<button
onClick={() => onDismiss(notification.id)}
className="ml-4 flex-shrink-0 inline-flex text-gray-400 hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
>
<span className="sr-only">Close</span>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
{/* Progress bar */}
<div className="absolute bottom-0 left-0 w-full h-1 bg-gray-700">
<div
className={`h-full ${progressColor} transition-all duration-100 ease-linear`}
style={{ width: `${progress}%` }}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,42 @@
/**
* Application constants
*/
import type { Status } from '@/components/ui/shadcn-io/kanban';
export const kanbanStatuses = [
{
id: 'pending',
title: 'Pending',
color: 'yellow',
className: 'text-yellow-600 border-yellow-600/20'
},
{
id: 'in-progress',
title: 'In Progress',
color: 'blue',
className: 'text-blue-600 border-blue-600/20'
},
{
id: 'review',
title: 'Review',
color: 'purple',
className: 'text-purple-600 border-purple-600/20'
},
{
id: 'done',
title: 'Done',
color: 'green',
className: 'text-green-600 border-green-600/20'
},
{
id: 'deferred',
title: 'Deferred',
color: 'gray',
className: 'text-gray-600 border-gray-600/20'
}
] as const;
export const CACHE_DURATION = 30000; // 30 seconds
export const REQUEST_TIMEOUT = 30000; // 30 seconds
export const HEADER_HEIGHT = 73; // Header with padding and border

Some files were not shown because too many files have changed in this diff Show More