feat(extension): add VS Code extension with kanban board interface
- Complete VS Code extension with React-based kanban board UI - MCP integration for real-time Task Master synchronization - Professional CI/CD workflows for marketplace publishing - Comprehensive configuration system with user preferences - ShadCN UI components with VS Code theme integration - Drag-and-drop task management with status transitions - AI-powered task features via MCP protocol - Robust error handling and connection management - Multi-registry publishing (VS Code Marketplace + Open VSX) - Security audit completed with hardcoded paths removed BREAKING CHANGE: Extension requires publisher setup and marketplace keys Publisher and extension naming decisions required: - Update publisher field from placeholder 'DavidMaliglowka' - Choose unique extension name (currently 'taskr-kanban') - Select appropriate extension icon - Configure CI secrets (VSCE_PAT, OVSX_PAT) for publishing See apps/extension/docs/ for detailed setup instructions
This commit is contained in:
203
.github/workflows/extension-ci.yml
vendored
Normal file
203
.github/workflows/extension-ci.yml
vendored
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
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
|
||||||
|
outputs:
|
||||||
|
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Generate cache key
|
||||||
|
id: cache-key
|
||||||
|
run: echo "key=${{ runner.os }}-extension-pnpm-${{ hashFiles('apps/extension/pnpm-lock.yaml') }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache pnpm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.pnpm-store
|
||||||
|
apps/extension/node_modules
|
||||||
|
key: ${{ steps.cache-key.outputs.key }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-extension-pnpm-
|
||||||
|
|
||||||
|
- name: Install Extension Dependencies
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
lint-and-typecheck:
|
||||||
|
needs: setup
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Restore pnpm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.pnpm-store
|
||||||
|
apps/extension/node_modules
|
||||||
|
key: ${{ needs.setup.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Install if cache miss
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm install --frozen-lockfile --prefer-offline
|
||||||
|
timeout-minutes: 3
|
||||||
|
|
||||||
|
- name: Lint Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm run lint
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Type Check Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm 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
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Restore pnpm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.pnpm-store
|
||||||
|
apps/extension/node_modules
|
||||||
|
key: ${{ needs.setup.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Install if cache miss
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm install --frozen-lockfile --prefer-offline
|
||||||
|
timeout-minutes: 3
|
||||||
|
|
||||||
|
- name: Build Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm run build
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Package Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm 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: pnpm exec 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
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: setup
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Restore pnpm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.pnpm-store
|
||||||
|
apps/extension/node_modules
|
||||||
|
key: ${{ needs.setup.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Install if cache miss
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm install --frozen-lockfile --prefer-offline
|
||||||
|
timeout-minutes: 3
|
||||||
|
|
||||||
|
- name: Run Extension Tests
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: xvfb-run -a pnpm run test
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: extension-test-results
|
||||||
|
path: apps/extension/test-results
|
||||||
|
retention-days: 30
|
||||||
240
.github/workflows/extension-release.yml
vendored
Normal file
240
.github/workflows/extension-release.yml
vendored
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
name: Extension Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'apps/extension/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
force_publish:
|
||||||
|
description: 'Force publish even without version changes'
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency: extension-release-${{ github.ref }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should-publish: ${{ steps.version-check.outputs.should-publish }}
|
||||||
|
current-version: ${{ steps.version-check.outputs.current-version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check version changes
|
||||||
|
id: version-check
|
||||||
|
run: |
|
||||||
|
# Get current version from package.json
|
||||||
|
CURRENT_VERSION=$(jq -r '.version' apps/extension/package.json)
|
||||||
|
echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Check if this is a force publish
|
||||||
|
if [ "${{ github.event.inputs.force_publish }}" = "true" ]; then
|
||||||
|
echo "should-publish=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Force publish requested"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if version changed in the last commit
|
||||||
|
if git diff HEAD~1 HEAD --name-only | grep -q "apps/extension/package.json\|apps/extension/package.publish.json"; then
|
||||||
|
# Check if version field actually changed
|
||||||
|
PREV_VERSION=$(git show HEAD~1:apps/extension/package.json | jq -r '.version')
|
||||||
|
if [ "$CURRENT_VERSION" != "$PREV_VERSION" ]; then
|
||||||
|
echo "should-publish=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version changed from $PREV_VERSION to $CURRENT_VERSION"
|
||||||
|
else
|
||||||
|
echo "should-publish=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "No version change detected"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "should-publish=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "No package.json changes detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-and-publish:
|
||||||
|
needs: check-version
|
||||||
|
if: needs.check-version.outputs.should-publish == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: extension-release
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Cache pnpm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.pnpm-store
|
||||||
|
apps/extension/node_modules
|
||||||
|
key: ${{ runner.os }}-extension-pnpm-${{ hashFiles('apps/extension/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-extension-pnpm-
|
||||||
|
|
||||||
|
- name: Install Extension Dependencies
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: xvfb-run -a pnpm run test
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
- name: Lint Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm run lint
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Type Check Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm run check-types
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Build Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm run build
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Package Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: pnpm run package
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Verify Package Structure
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: |
|
||||||
|
echo "=== Checking vsix-build structure ==="
|
||||||
|
ls -la vsix-build/
|
||||||
|
echo "=== Checking dist contents ==="
|
||||||
|
ls -la vsix-build/dist/
|
||||||
|
echo "=== Verifying required files ==="
|
||||||
|
test -f vsix-build/package.json || (echo "Missing package.json" && exit 1)
|
||||||
|
test -f vsix-build/dist/extension.js || (echo "Missing extension.js" && exit 1)
|
||||||
|
echo "=== Checking package.json content ==="
|
||||||
|
cat vsix-build/package.json | jq '.name, .version, .publisher'
|
||||||
|
|
||||||
|
- name: Create VSIX Package
|
||||||
|
working-directory: apps/extension/vsix-build
|
||||||
|
run: pnpm exec vsce package --no-dependencies
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Get VSIX filename
|
||||||
|
id: vsix-info
|
||||||
|
working-directory: apps/extension/vsix-build
|
||||||
|
run: |
|
||||||
|
VSIX_FILE=$(ls *.vsix)
|
||||||
|
echo "vsix-filename=$VSIX_FILE" >> $GITHUB_OUTPUT
|
||||||
|
echo "Found VSIX: $VSIX_FILE"
|
||||||
|
|
||||||
|
- name: Validate VSIX Package
|
||||||
|
working-directory: apps/extension/vsix-build
|
||||||
|
run: |
|
||||||
|
echo "=== VSIX Package Contents ==="
|
||||||
|
unzip -l "${{ steps.vsix-info.outputs.vsix-filename }}"
|
||||||
|
|
||||||
|
- name: Publish to VS Code Marketplace
|
||||||
|
working-directory: apps/extension/vsix-build
|
||||||
|
run: pnpm exec 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: extension-v${{ needs.check-version.outputs.current-version }}
|
||||||
|
release_name: Extension v${{ needs.check-version.outputs.current-version }}
|
||||||
|
body: |
|
||||||
|
VS Code Extension Release v${{ needs.check-version.outputs.current-version }}
|
||||||
|
|
||||||
|
**Changes in this release:**
|
||||||
|
- Published to VS Code Marketplace
|
||||||
|
- Published to Open VSX Registry
|
||||||
|
- Extension package: `${{ steps.vsix-info.outputs.vsix-filename }}`
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
- Install from VS Code Marketplace: [Task Master Kanban](https://marketplace.visualstudio.com/items?itemName=[TBD])
|
||||||
|
- Install from Open VSX Registry: [Task Master Kanban](https://open-vsx.org/extension/[TBD])
|
||||||
|
- Or download the VSIX file below and install manually
|
||||||
|
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-v${{ needs.check-version.outputs.current-version }}
|
||||||
|
path: |
|
||||||
|
apps/extension/vsix-build/*.vsix
|
||||||
|
apps/extension/dist/
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
notify-success:
|
||||||
|
needs: [check-version, build-and-publish]
|
||||||
|
if: success() && needs.check-version.outputs.should-publish == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Success Notification
|
||||||
|
run: |
|
||||||
|
echo "🎉 Extension v${{ needs.check-version.outputs.current-version }} successfully published!"
|
||||||
|
echo "📦 Available on VS Code Marketplace"
|
||||||
|
echo "🌍 Available on Open VSX Registry"
|
||||||
|
echo "🏷️ GitHub release created: extension-v${{ needs.check-version.outputs.current-version }}"
|
||||||
|
|
||||||
|
notify-skipped:
|
||||||
|
needs: check-version
|
||||||
|
if: needs.check-version.outputs.should-publish == 'false'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Skip Notification
|
||||||
|
run: |
|
||||||
|
echo "ℹ️ Extension publish skipped - no version changes detected"
|
||||||
|
echo "Current version: ${{ needs.check-version.outputs.current-version }}"
|
||||||
|
echo "To force publish, use workflow_dispatch with force_publish=true"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -87,3 +87,6 @@ dev-debug.log
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# apps/extension
|
||||||
|
apps/extension/vsix-build/
|
||||||
25
apps/extension/.vscodeignore
Normal file
25
apps/extension/.vscodeignore
Normal 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
|
||||||
|
|
||||||
|
|
||||||
9
apps/extension/CHANGELOG.md
Normal file
9
apps/extension/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to the vscode extension will be documented in this file.
|
||||||
|
|
||||||
|
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Initial release
|
||||||
21
apps/extension/LICENSE
Normal file
21
apps/extension/LICENSE
Normal 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.
|
||||||
199
apps/extension/README.md
Normal file
199
apps/extension/README.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# Official Task Master AI Extension
|
||||||
|
|
||||||
|
Transform your [Task Master AI](https://github.com/TaskMasterEYJ/task-master-ai) projects into a beautiful, interactive Kanban board directly in VS Code. Drag, drop, and manage your tasks with ease while maintaining real-time synchronization with your Task Master project files.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 🎯 **Visual Task Management**
|
||||||
|
- **Drag & Drop Kanban Board** - Intuitive task management with visual columns
|
||||||
|
- **Real-time Synchronization** - Changes sync instantly with your Task Master project files
|
||||||
|
- **Status Columns** - To Do, In Progress, Review, Done, and Deferred
|
||||||
|
- **Task Details View** - View and edit implementation details, test strategies, and notes
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 🤖 **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 Task Master AI via Model Context Protocol
|
||||||
|
- **Intelligent Caching** - Smart performance optimization with background refresh
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 🚀 **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 Task Master 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 Task Master Project**
|
||||||
|
If you don't have a Task Master project yet:
|
||||||
|
```bash
|
||||||
|
cd your-project
|
||||||
|
npx task-master-ai init
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Open Kanban Board**
|
||||||
|
- **Command Palette** (Ctrl+Shift+P): `Task Master 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 Task Master MCP server connection:
|
||||||
|
- **No manual installation required** - The extension spawns the MCP server automatically
|
||||||
|
- **Uses npx by default** - Automatically downloads Task Master 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
|
||||||
|
|
||||||
|
### **Managing Tasks**
|
||||||
|
|
||||||
|
| Action | How To |
|
||||||
|
|--------|--------|
|
||||||
|
| **Change Task Status** | Drag task to different column |
|
||||||
|
| **View Details** | Click on any task |
|
||||||
|
| **Edit Task** | Click task, then use edit controls |
|
||||||
|
| **Add Subtask** | Click + button on parent task |
|
||||||
|
| **Use AI Features** | Open task details and use AI panel |
|
||||||
|
|
||||||
|
### **Kanban Columns**
|
||||||
|
|
||||||
|
- **📝 To Do** - Tasks ready to be worked on
|
||||||
|
- **⚡ In Progress** - Currently active tasks
|
||||||
|
- **👀 Review** - Tasks pending review or approval
|
||||||
|
- **✅ Done** - Completed tasks
|
||||||
|
- **⏸️ Deferred** - Postponed or blocked tasks
|
||||||
|
|
||||||
|
### **AI-Powered Task Management**
|
||||||
|
|
||||||
|
The extension integrates seamlessly with Task Master 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 "Task Master":
|
||||||
|
|
||||||
|
### **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 → Task Master 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: `Task Master 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**: [Task Master 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)
|
||||||
|
|
||||||
|
### **Related Projects**
|
||||||
|
- 📡 **MCP Protocol**: [Model Context Protocol](https://modelcontextprotocol.io/)
|
||||||
|
|
||||||
|
## 🎯 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 Task Master Kanban?
|
||||||
|
|
||||||
|
✅ **Visual workflow management** for your Task Master 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.
|
||||||
BIN
apps/extension/assets/banner.png
Normal file
BIN
apps/extension/assets/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
BIN
apps/extension/assets/icon.png
Normal file
BIN
apps/extension/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
apps/extension/assets/screenshots/kanban-board.png
Normal file
BIN
apps/extension/assets/screenshots/kanban-board.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 351 KiB |
BIN
apps/extension/assets/screenshots/task-details.png
Normal file
BIN
apps/extension/assets/screenshots/task-details.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
18
apps/extension/components.json
Normal file
18
apps/extension/components.json
Normal 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"
|
||||||
|
}
|
||||||
159
apps/extension/docs/extension-CI-setup.md
Normal file
159
apps/extension/docs/extension-CI-setup.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# VS Code Extension CI/CD Setup
|
||||||
|
|
||||||
|
This document explains the CI/CD setup for the Task Master VS Code extension.
|
||||||
|
|
||||||
|
## 🔄 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 (`pnpm run build`)
|
||||||
|
- 📦 Creates a clean package (`pnpm run package`)
|
||||||
|
- 🧪 Runs tests with VS Code test framework
|
||||||
|
- 📋 Creates a test VSIX package to verify packaging works
|
||||||
|
- 💾 Uploads build artifacts for inspection
|
||||||
|
|
||||||
|
### 2. Extension Release (`extension-release.yml`)
|
||||||
|
**Triggers:**
|
||||||
|
- Push to `main` branch (only when extension files change AND version changes)
|
||||||
|
- Manual trigger with `workflow_dispatch` (with optional force publish)
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- 🔍 Checks if the extension version changed
|
||||||
|
- 🧪 Runs full test suite (lint, typecheck, tests)
|
||||||
|
- 🔨 Builds and packages the extension
|
||||||
|
- 📤 Publishes to VS Code Marketplace
|
||||||
|
- 🌍 Publishes to Open VSX Registry (for VSCodium, Gitpod, etc.)
|
||||||
|
- 🏷️ Creates a GitHub release with the VSIX file
|
||||||
|
- 📊 Uploads release artifacts
|
||||||
|
|
||||||
|
## 🔑 Required Secrets
|
||||||
|
|
||||||
|
To use the release workflow, 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.
|
||||||
|
|
||||||
|
## 🚀 Publishing Process
|
||||||
|
|
||||||
|
### Automatic Publishing (Recommended)
|
||||||
|
1. **Make changes** to the extension code
|
||||||
|
2. **Update version** in both:
|
||||||
|
- `apps/extension/package.json`
|
||||||
|
- `apps/extension/package.publish.json`
|
||||||
|
3. **Commit and push** to `main` branch
|
||||||
|
4. **CI automatically triggers** and publishes if version changed
|
||||||
|
|
||||||
|
### Manual Publishing
|
||||||
|
1. Go to **Actions** tab in GitHub
|
||||||
|
2. Select **Extension Release** workflow
|
||||||
|
3. Click **Run workflow**
|
||||||
|
4. Check **"Force publish even without version changes"** if needed
|
||||||
|
5. Click **Run workflow**
|
||||||
|
|
||||||
|
## 📋 Version Management
|
||||||
|
|
||||||
|
### Version Sync Checklist
|
||||||
|
When updating the extension version, ensure these fields match in both files:
|
||||||
|
|
||||||
|
**`package.json` and `package.publish.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0.2", // ⚠️ MUST MATCH
|
||||||
|
"publisher": "DavidMaliglowka", // ⚠️ MUST MATCH
|
||||||
|
"displayName": "Task Master Kanban", // ⚠️ MUST MATCH
|
||||||
|
"description": "...", // ⚠️ MUST MATCH
|
||||||
|
"engines": { "vscode": "^1.93.0" }, // ⚠️ MUST MATCH
|
||||||
|
"categories": [...], // ⚠️ MUST MATCH
|
||||||
|
"contributes": { ... } // ⚠️ MUST MATCH
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Detection Logic
|
||||||
|
The release workflow only publishes when:
|
||||||
|
- Extension files changed in the push, AND
|
||||||
|
- Version field changed in `package.json` or `package.publish.json`
|
||||||
|
|
||||||
|
## 🔍 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
|
||||||
|
|
||||||
|
### Release Status
|
||||||
|
- **Published 🎉**: Extension live on VS Code Marketplace
|
||||||
|
- **Skipped ℹ️**: No version changes detected
|
||||||
|
- **Failed ❌**: Check logs - often missing secrets or build issues
|
||||||
|
|
||||||
|
### Artifacts
|
||||||
|
Both workflows upload artifacts that you can download:
|
||||||
|
- **CI**: Test results, built files, and VSIX package
|
||||||
|
- **Release**: Final VSIX package and build artifacts (90-day retention)
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**"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
|
||||||
|
|
||||||
|
**"Version not changed" Skipped Release**
|
||||||
|
- Update version in both `package.json` AND `package.publish.json`
|
||||||
|
- Ensure files are committed and pushed
|
||||||
|
- Use manual trigger with force publish if needed
|
||||||
|
|
||||||
|
**Build Failures**
|
||||||
|
- Check extension code compiles locally: `cd apps/extension && pnpm run build`
|
||||||
|
- Verify tests pass locally: `pnpm run test`
|
||||||
|
- Check for TypeScript errors: `pnpm run check-types`
|
||||||
|
|
||||||
|
**Packaging Failures**
|
||||||
|
- Ensure clean package builds: `pnpm run package`
|
||||||
|
- Check vsix-build structure is correct
|
||||||
|
- Verify package.publish.json has correct fields
|
||||||
|
|
||||||
|
## 📁 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
|
||||||
|
|
||||||
|
This ensures clean, conflict-free publishing to both VS Code Marketplace and Open VSX Registry! 🚀
|
||||||
|
|
||||||
|
## 🌍 **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
|
||||||
177
apps/extension/docs/extension-development-guide.md
Normal file
177
apps/extension/docs/extension-development-guide.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# 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
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Start development with hot reload
|
||||||
|
pnpm run watch
|
||||||
|
|
||||||
|
# Run just JavaScript build
|
||||||
|
pnpm run build:js
|
||||||
|
|
||||||
|
# Run just CSS build
|
||||||
|
pnpm run build:css
|
||||||
|
|
||||||
|
# Full production build
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
pnpm run check-types
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
pnpm 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
|
||||||
|
pnpm run package
|
||||||
|
```
|
||||||
|
This creates `vsix-build/` with clean distribution files.
|
||||||
|
|
||||||
|
### Step 2: Create VSIX
|
||||||
|
```bash
|
||||||
|
cd vsix-build
|
||||||
|
pnpm exec vsce package --no-dependencies
|
||||||
|
```
|
||||||
|
Creates: `taskr-kanban-1.0.1.vsix`
|
||||||
|
|
||||||
|
### Alternative: One Command
|
||||||
|
```bash
|
||||||
|
pnpm run package && cd vsix-build && pnpm exec 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": "DavidMaliglowka", // ⚠️ 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 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
|
||||||
|
|
||||||
|
## 📝 Version Release Checklist
|
||||||
|
|
||||||
|
1. **Update version** in both `package.json` and `package.publish.json`
|
||||||
|
2. **Update CHANGELOG.md** with new features/fixes
|
||||||
|
3. **Test locally** with `F5` in VS Code
|
||||||
|
4. **Build clean package**: `pnpm run package`
|
||||||
|
5. **Test packaged extension**: Install `.vsix` file
|
||||||
|
6. **Publish**: Upload to marketplace or distribute `.vsix`
|
||||||
|
|
||||||
|
## 🎯 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: Always use `pnpm run package` → `cd vsix-build` → `vsce package --no-dependencies` for production builds! 🚀
|
||||||
136
apps/extension/esbuild.js
Normal file
136
apps/extension/esbuild.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
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,
|
||||||
|
sourcesContent: false,
|
||||||
|
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,
|
||||||
|
sourcesContent: false,
|
||||||
|
platform: 'browser',
|
||||||
|
outdir: 'dist',
|
||||||
|
logLevel: 'silent',
|
||||||
|
target: ['es2020'],
|
||||||
|
jsx: 'automatic',
|
||||||
|
jsxImportSource: 'react',
|
||||||
|
external: ['*.css'],
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (watch) {
|
||||||
|
await Promise.all([
|
||||||
|
extensionCtx.watch(),
|
||||||
|
webviewCtx.watch()
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
await Promise.all([
|
||||||
|
extensionCtx.rebuild(),
|
||||||
|
webviewCtx.rebuild()
|
||||||
|
]);
|
||||||
|
await extensionCtx.dispose();
|
||||||
|
await webviewCtx.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
28
apps/extension/eslint.config.mjs
Normal file
28
apps/extension/eslint.config.mjs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||||
|
import tsParser from "@typescript-eslint/parser";
|
||||||
|
|
||||||
|
export default [{
|
||||||
|
files: ["**/*.ts"],
|
||||||
|
}, {
|
||||||
|
plugins: {
|
||||||
|
"@typescript-eslint": typescriptEslint,
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/naming-convention": ["warn", {
|
||||||
|
selector: "import",
|
||||||
|
format: ["camelCase", "PascalCase"],
|
||||||
|
}],
|
||||||
|
|
||||||
|
curly: "warn",
|
||||||
|
eqeqeq: "warn",
|
||||||
|
"no-throw-literal": "warn",
|
||||||
|
semi: "warn",
|
||||||
|
},
|
||||||
|
}];
|
||||||
@@ -1,15 +1,268 @@
|
|||||||
{
|
{
|
||||||
"name": "extension",
|
"name": "taskr",
|
||||||
"version": "0.20.0",
|
"displayName": "Task Master Kanban",
|
||||||
"main": "index.js",
|
"description": "A visual Kanban board interface for Task Master projects in VS Code",
|
||||||
"scripts": {
|
"version": "1.0.0",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"publisher": "DavidMaliglowka",
|
||||||
},
|
"icon": "assets/icon.png",
|
||||||
"keywords": [],
|
"engines": {
|
||||||
"author": "",
|
"vscode": "^1.93.0"
|
||||||
"license": "ISC",
|
},
|
||||||
"description": "",
|
"categories": [
|
||||||
"devDependencies": {
|
"AI",
|
||||||
"typescript": "^5.8.3"
|
"Visualization",
|
||||||
}
|
"Education",
|
||||||
|
"Other"
|
||||||
|
],
|
||||||
|
"main": "./dist/extension.js",
|
||||||
|
"contributes": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "taskr.showKanbanBoard",
|
||||||
|
"title": "Task Master Kanban: Show Board"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "taskr.checkConnection",
|
||||||
|
"title": "Task Master Kanban: Check Connection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "taskr.reconnect",
|
||||||
|
"title": "Task Master Kanban: Reconnect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "taskr.openSettings",
|
||||||
|
"title": "Task Master Kanban: Open Settings"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration": {
|
||||||
|
"title": "Task Master Kanban",
|
||||||
|
"properties": {
|
||||||
|
"taskmaster.mcp.command": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "npx",
|
||||||
|
"description": "The command or absolute path to execute for the MCP server (e.g., 'npx' or '/usr/local/bin/task-master-ai')."
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.args": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": [
|
||||||
|
"-y",
|
||||||
|
"--package=task-master-ai",
|
||||||
|
"task-master-ai"
|
||||||
|
],
|
||||||
|
"description": "An array of arguments to pass to the MCP server command."
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.cwd": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Working directory for the Task Master MCP server (defaults to workspace root)"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.env": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Environment variables for the Task Master MCP server"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.timeout": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 30000,
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"description": "Connection timeout in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.maxReconnectAttempts": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
"description": "Maximum number of reconnection attempts"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.reconnectBackoffMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 1000,
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 10000,
|
||||||
|
"description": "Initial reconnection backoff delay in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.maxBackoffMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 30000,
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"description": "Maximum reconnection backoff delay in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.healthCheckIntervalMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 15000,
|
||||||
|
"minimum": 5000,
|
||||||
|
"maximum": 60000,
|
||||||
|
"description": "Health check interval in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.autoRefresh": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Automatically refresh tasks from the server"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.refreshIntervalMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 10000,
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"description": "Auto-refresh interval in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.theme": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"auto",
|
||||||
|
"light",
|
||||||
|
"dark"
|
||||||
|
],
|
||||||
|
"default": "auto",
|
||||||
|
"description": "UI theme preference"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.showCompletedTasks": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Show completed tasks in the Kanban board"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.taskDisplayLimit": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 1000,
|
||||||
|
"description": "Maximum number of tasks to display"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.showPriority": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Show task priority indicators"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.showTaskIds": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Show task IDs in the interface"
|
||||||
|
},
|
||||||
|
"taskmaster.performance.maxConcurrentRequests": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
"description": "Maximum number of concurrent MCP requests"
|
||||||
|
},
|
||||||
|
"taskmaster.performance.requestTimeoutMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 30000,
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"description": "Request timeout in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.performance.cacheTasksMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5000,
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 60000,
|
||||||
|
"description": "Task cache duration in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.performance.lazyLoadThreshold": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 50,
|
||||||
|
"minimum": 10,
|
||||||
|
"maximum": 500,
|
||||||
|
"description": "Number of tasks before enabling lazy loading"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.enableLogging": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Enable debug logging"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.logLevel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"error",
|
||||||
|
"warn",
|
||||||
|
"info",
|
||||||
|
"debug"
|
||||||
|
],
|
||||||
|
"default": "info",
|
||||||
|
"description": "Logging level"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.enableConnectionMetrics": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Enable connection performance metrics"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.saveEventLogs": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Save event logs to files"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.maxEventLogSize": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 1000,
|
||||||
|
"minimum": 10,
|
||||||
|
"maximum": 10000,
|
||||||
|
"description": "Maximum number of events to keep in memory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"vscode:prepublish": false,
|
||||||
|
"build": "pnpm run build:js && pnpm run build:css",
|
||||||
|
"build:js": "node ./esbuild.js --production",
|
||||||
|
"build:css": "npx @tailwindcss/cli -o ./dist/index.css --minify",
|
||||||
|
"package": "pnpm exec node ./package.mjs",
|
||||||
|
"package:direct": "node ./package.mjs",
|
||||||
|
"debug:env": "node ./debug-env.mjs",
|
||||||
|
"compile": "node ./esbuild.js",
|
||||||
|
"watch": "pnpm run watch:js & pnpm run watch:css",
|
||||||
|
"watch:js": "node ./esbuild.js --watch",
|
||||||
|
"watch:css": "npx @tailwindcss/cli -o ./dist/index.css --watch",
|
||||||
|
"lint": "eslint src --ext ts,tsx",
|
||||||
|
"test": "vscode-test",
|
||||||
|
"check-types": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@modelcontextprotocol/sdk": "1.13.3",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-portal": "^1.1.9",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
|
"@types/mocha": "^10.0.10",
|
||||||
|
"@types/node": "20.x",
|
||||||
|
"@types/react": "19.1.8",
|
||||||
|
"@types/react-dom": "19.1.6",
|
||||||
|
"@types/vscode": "^1.101.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||||
|
"@typescript-eslint/parser": "^8.31.1",
|
||||||
|
"@vscode/test-cli": "^0.0.11",
|
||||||
|
"@vscode/test-electron": "^2.5.2",
|
||||||
|
"@vscode/vsce": "^2.32.0",
|
||||||
|
"autoprefixer": "10.4.21",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"esbuild": "^0.25.3",
|
||||||
|
"esbuild-postcss": "^0.0.4",
|
||||||
|
"eslint": "^9.25.1",
|
||||||
|
"fs-extra": "^11.3.0",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"postcss": "8.5.6",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss": "4.1.11",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"glob@<8": "^10.4.5",
|
||||||
|
"inflight": "npm:@tootallnate/once@2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
apps/extension/package.mjs
Normal file
94
apps/extension/package.mjs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
// --- 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('pnpm run build:js', { stdio: 'inherit' });
|
||||||
|
console.log('\nBuilding CSS...');
|
||||||
|
execSync('pnpm run build:css', { stdio: 'inherit' });
|
||||||
|
|
||||||
|
// 2. Prepare Clean Directory
|
||||||
|
console.log(`\nPreparing clean directory at: ${packageDir}`);
|
||||||
|
fs.emptyDirSync(packageDir);
|
||||||
|
|
||||||
|
// 3. Copy Build Artifacts (excluding source maps)
|
||||||
|
console.log('Copying build artifacts...');
|
||||||
|
const distDir = path.resolve(__dirname, 'dist');
|
||||||
|
const targetDistDir = path.resolve(packageDir, 'dist');
|
||||||
|
fs.ensureDirSync(targetDistDir);
|
||||||
|
|
||||||
|
// Only copy the files we need (exclude .map files)
|
||||||
|
const filesToCopy = ['extension.js', 'index.js', 'index.css'];
|
||||||
|
for (const file of filesToCopy) {
|
||||||
|
const srcFile = path.resolve(distDir, file);
|
||||||
|
const destFile = path.resolve(targetDistDir, file);
|
||||||
|
if (fs.existsSync(srcFile)) {
|
||||||
|
fs.copySync(srcFile, destFile);
|
||||||
|
console.log(` - Copied dist/${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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. Copy and RENAME the clean manifest
|
||||||
|
console.log('Copying and preparing the final package.json...');
|
||||||
|
fs.copySync(path.resolve(__dirname, 'package.publish.json'), path.resolve(packageDir, 'package.json'));
|
||||||
|
console.log(' - Copied package.publish.json as package.json');
|
||||||
|
|
||||||
|
// 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 && pnpm exec vsce package --no-dependencies`);
|
||||||
|
|
||||||
|
// Read version from package.publish.json
|
||||||
|
const publishPackage = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package.publish.json'), 'utf8'));
|
||||||
|
const version = publishPackage.version;
|
||||||
|
console.log(`\nYour extension will be packaged to: vsix-build/taskr-${version}.vsix`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Packaging failed!');
|
||||||
|
console.error(error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
248
apps/extension/package.publish.json
Normal file
248
apps/extension/package.publish.json
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
{
|
||||||
|
"name": "taskr-kanban",
|
||||||
|
"displayName": "taskr: Task Master Kanban",
|
||||||
|
"description": "A visual Kanban board interface for Task Master projects in VS Code",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "DavidMaliglowka",
|
||||||
|
"icon": "assets/icon.png",
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.93.0"
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
"AI",
|
||||||
|
"Visualization",
|
||||||
|
"Education",
|
||||||
|
"Other"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"kanban",
|
||||||
|
"kanban board",
|
||||||
|
"productivity",
|
||||||
|
"todo",
|
||||||
|
"task tracking",
|
||||||
|
"project management",
|
||||||
|
"task-master",
|
||||||
|
"task management",
|
||||||
|
"agile",
|
||||||
|
"scrum",
|
||||||
|
"ai",
|
||||||
|
"mcp",
|
||||||
|
"model context protocol",
|
||||||
|
"dashboard",
|
||||||
|
"chatgpt",
|
||||||
|
"claude",
|
||||||
|
"openai",
|
||||||
|
"anthropic",
|
||||||
|
"task",
|
||||||
|
"npm",
|
||||||
|
"intellicode",
|
||||||
|
"react",
|
||||||
|
"typescript",
|
||||||
|
"php",
|
||||||
|
"python",
|
||||||
|
"node",
|
||||||
|
"planner",
|
||||||
|
"organizer",
|
||||||
|
"workflow",
|
||||||
|
"boards",
|
||||||
|
"cards"
|
||||||
|
],
|
||||||
|
"repository": "https://github.com/eyaltoledano/claude-task-master",
|
||||||
|
"activationEvents": [
|
||||||
|
"onCommand:taskr.showKanbanBoard",
|
||||||
|
"onCommand:taskr.checkConnection",
|
||||||
|
"onCommand:taskr.reconnect",
|
||||||
|
"onCommand:taskr.openSettings"
|
||||||
|
],
|
||||||
|
"main": "./dist/extension.js",
|
||||||
|
"contributes": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "taskr.showKanbanBoard",
|
||||||
|
"title": "Task Master Kanban: Show Board"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "taskr.checkConnection",
|
||||||
|
"title": "Task Master Kanban: Check Connection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "taskr.reconnect",
|
||||||
|
"title": "Task Master Kanban: Reconnect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "taskr.openSettings",
|
||||||
|
"title": "Task Master Kanban: Open Settings"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration": {
|
||||||
|
"title": "Task Master Kanban",
|
||||||
|
"properties": {
|
||||||
|
"taskmaster.mcp.command": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "npx",
|
||||||
|
"description": "The command or absolute path to execute for the MCP server (e.g., 'npx' or '/usr/local/bin/task-master-ai')."
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.args": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": [
|
||||||
|
"-y",
|
||||||
|
"--package=task-master-ai",
|
||||||
|
"task-master-ai"
|
||||||
|
],
|
||||||
|
"description": "An array of arguments to pass to the MCP server command."
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.cwd": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Working directory for the Task Master MCP server (defaults to workspace root)"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.env": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Environment variables for the Task Master MCP server"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.timeout": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 30000,
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"description": "Connection timeout in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.maxReconnectAttempts": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
"description": "Maximum number of reconnection attempts"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.reconnectBackoffMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 1000,
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 10000,
|
||||||
|
"description": "Initial reconnection backoff delay in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.maxBackoffMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 30000,
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"description": "Maximum reconnection backoff delay in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.mcp.healthCheckIntervalMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 15000,
|
||||||
|
"minimum": 5000,
|
||||||
|
"maximum": 60000,
|
||||||
|
"description": "Health check interval in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.autoRefresh": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Automatically refresh tasks from the server"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.refreshIntervalMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 10000,
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"description": "Auto-refresh interval in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.theme": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"auto",
|
||||||
|
"light",
|
||||||
|
"dark"
|
||||||
|
],
|
||||||
|
"default": "auto",
|
||||||
|
"description": "UI theme preference"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.showCompletedTasks": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Show completed tasks in the Kanban board"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.taskDisplayLimit": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 1000,
|
||||||
|
"description": "Maximum number of tasks to display"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.showPriority": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Show task priority indicators"
|
||||||
|
},
|
||||||
|
"taskmaster.ui.showTaskIds": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Show task IDs in the interface"
|
||||||
|
},
|
||||||
|
"taskmaster.performance.maxConcurrentRequests": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
"description": "Maximum number of concurrent MCP requests"
|
||||||
|
},
|
||||||
|
"taskmaster.performance.requestTimeoutMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 30000,
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"description": "Request timeout in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.performance.cacheTasksMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5000,
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 60000,
|
||||||
|
"description": "Task cache duration in milliseconds"
|
||||||
|
},
|
||||||
|
"taskmaster.performance.lazyLoadThreshold": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 50,
|
||||||
|
"minimum": 10,
|
||||||
|
"maximum": 500,
|
||||||
|
"description": "Number of tasks before enabling lazy loading"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.enableLogging": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Enable debug logging"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.logLevel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"error",
|
||||||
|
"warn",
|
||||||
|
"info",
|
||||||
|
"debug"
|
||||||
|
],
|
||||||
|
"default": "info",
|
||||||
|
"description": "Logging level"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.enableConnectionMetrics": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Enable connection performance metrics"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.saveEventLogs": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Save event logs to files"
|
||||||
|
},
|
||||||
|
"taskmaster.debug.maxEventLogSize": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 1000,
|
||||||
|
"minimum": 10,
|
||||||
|
"maximum": 10000,
|
||||||
|
"description": "Maximum number of events to keep in memory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6366
apps/extension/pnpm-lock.yaml
generated
Normal file
6366
apps/extension/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1248
apps/extension/src/components/TaskDetailsView.tsx
Normal file
1248
apps/extension/src/components/TaskDetailsView.tsx
Normal file
File diff suppressed because it is too large
Load Diff
46
apps/extension/src/components/ui/badge.tsx
Normal file
46
apps/extension/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
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 }
|
||||||
109
apps/extension/src/components/ui/breadcrumb.tsx
Normal file
109
apps/extension/src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-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,
|
||||||
|
}
|
||||||
59
apps/extension/src/components/ui/button.tsx
Normal file
59
apps/extension/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
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 }
|
||||||
92
apps/extension/src/components/ui/card.tsx
Normal file
92
apps/extension/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * 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,
|
||||||
|
}
|
||||||
31
apps/extension/src/components/ui/collapsible.tsx
Normal file
31
apps/extension/src/components/ui/collapsible.tsx
Normal 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 }
|
||||||
257
apps/extension/src/components/ui/dropdown-menu.tsx
Normal file
257
apps/extension/src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
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(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: 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(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
}
|
||||||
22
apps/extension/src/components/ui/label.tsx
Normal file
22
apps/extension/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
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 }
|
||||||
56
apps/extension/src/components/ui/scroll-area.tsx
Normal file
56
apps/extension/src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
|
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", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: 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 }
|
||||||
28
apps/extension/src/components/ui/separator.tsx
Normal file
28
apps/extension/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
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 }
|
||||||
186
apps/extension/src/components/ui/shadcn-io/kanban/index.tsx
Normal file
186
apps/extension/src/components/ui/shadcn-io/kanban/index.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragOverlay,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
rectIntersection,
|
||||||
|
useDraggable,
|
||||||
|
useDroppable,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
|
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;
|
||||||
|
className?: string;
|
||||||
|
dragOverlay?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KanbanProvider = ({
|
||||||
|
children,
|
||||||
|
onDragEnd,
|
||||||
|
onDragStart,
|
||||||
|
className,
|
||||||
|
dragOverlay,
|
||||||
|
}: KanbanProviderProps) => {
|
||||||
|
// Configure sensors with activation constraints to prevent accidental drags
|
||||||
|
const sensors = useSensors(
|
||||||
|
// Only start a drag if you've moved more than 8px
|
||||||
|
useSensor(MouseSensor, {
|
||||||
|
activationConstraint: { distance: 8 },
|
||||||
|
}),
|
||||||
|
// On touch devices, require a short press + small move
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: { delay: 150, tolerance: 5 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={rectIntersection}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn('grid w-full auto-cols-fr grid-flow-col gap-4', className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<DragOverlay>
|
||||||
|
{dragOverlay}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
};
|
||||||
18
apps/extension/src/components/ui/textarea.tsx
Normal file
18
apps/extension/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * 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 }
|
||||||
1166
apps/extension/src/extension.ts
Normal file
1166
apps/extension/src/extension.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
console.log('hello world');
|
|
||||||
6
apps/extension/src/lib/utils.ts
Normal file
6
apps/extension/src/lib/utils.ts
Normal 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));
|
||||||
|
}
|
||||||
15
apps/extension/src/test/extension.test.ts
Normal file
15
apps/extension/src/test/extension.test.ts
Normal 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
399
apps/extension/src/utils/configManager.ts
Normal file
399
apps/extension/src/utils/configManager.ts
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { 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: ['-y', '--package=task-master-ai', 'task-master-ai'],
|
||||||
|
cwd: vscode.workspace.rootPath || '',
|
||||||
|
env: undefined,
|
||||||
|
timeout: 30000,
|
||||||
|
maxReconnectAttempts: 5,
|
||||||
|
reconnectBackoffMs: 1000,
|
||||||
|
maxBackoffMs: 30000,
|
||||||
|
healthCheckIntervalMs: 15000
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
autoRefresh: true,
|
||||||
|
refreshIntervalMs: 10000,
|
||||||
|
theme: 'auto',
|
||||||
|
showCompletedTasks: true,
|
||||||
|
taskDisplayLimit: 100,
|
||||||
|
showPriority: true,
|
||||||
|
showTaskIds: true
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
maxConcurrentRequests: 5,
|
||||||
|
requestTimeoutMs: 30000,
|
||||||
|
cacheTasksMs: 5000,
|
||||||
|
lazyLoadThreshold: 50
|
||||||
|
},
|
||||||
|
debug: {
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: 'info',
|
||||||
|
enableConnectionMetrics: true,
|
||||||
|
saveEventLogs: false,
|
||||||
|
maxEventLogSize: 1000
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup configuration watcher
|
||||||
|
*/
|
||||||
|
private setupConfigWatcher(): void {
|
||||||
|
vscode.workspace.onDidChangeConfiguration((event) => {
|
||||||
|
if (event.affectsConfiguration('taskmaster')) {
|
||||||
|
console.log('Task Master configuration changed, reloading...');
|
||||||
|
this.config = this.loadConfig();
|
||||||
|
this.notifyConfigChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge configurations
|
||||||
|
*/
|
||||||
|
private mergeConfig(baseConfig: TaskMasterConfig, updates: Partial<TaskMasterConfig>): TaskMasterConfig {
|
||||||
|
return {
|
||||||
|
mcp: { ...baseConfig.mcp, ...updates.mcp },
|
||||||
|
ui: { ...baseConfig.ui, ...updates.ui },
|
||||||
|
performance: { ...baseConfig.performance, ...updates.performance },
|
||||||
|
debug: { ...baseConfig.debug, ...updates.debug }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify configuration change listeners
|
||||||
|
*/
|
||||||
|
private notifyConfigChange(): void {
|
||||||
|
this.configListeners.forEach(listener => {
|
||||||
|
try {
|
||||||
|
listener(this.config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in configuration change listener:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to get configuration manager instance
|
||||||
|
*/
|
||||||
|
export function getConfigManager(): ConfigManager {
|
||||||
|
return ConfigManager.getInstance();
|
||||||
|
}
|
||||||
354
apps/extension/src/utils/connectionManager.ts
Normal file
354
apps/extension/src/utils/connectionManager.ts
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { MCPClientManager, MCPConfig, 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() });
|
||||||
|
|
||||||
|
console.log('Connection manager: Successfully connected');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logEvent({
|
||||||
|
type: 'error',
|
||||||
|
timestamp: new Date(),
|
||||||
|
data: { error: error instanceof Error ? error.message : 'Unknown error' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.handleConnectionFailure(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: number = 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) {
|
||||||
|
console.log('Multiple consecutive failures detected, attempting reconnection...');
|
||||||
|
this.reconnectWithBackoff().catch(err => {
|
||||||
|
console.error('Reconnection failed:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyConnectionChange();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update configuration and reconnect
|
||||||
|
*/
|
||||||
|
async updateConfig(newConfig: MCPConfig): Promise<void> {
|
||||||
|
this.config = newConfig;
|
||||||
|
|
||||||
|
await this.disconnect();
|
||||||
|
this.mcpClient = new MCPClientManager(newConfig);
|
||||||
|
|
||||||
|
// Attempt to reconnect with new config
|
||||||
|
try {
|
||||||
|
await this.connect();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect with new configuration:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start health monitoring
|
||||||
|
*/
|
||||||
|
private startHealthMonitoring(): void {
|
||||||
|
this.stopHealthMonitoring();
|
||||||
|
|
||||||
|
this.healthCheckInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await this.testConnection();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Health check failed:', error);
|
||||||
|
}
|
||||||
|
}, 15000); // Check every 15 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop health monitoring
|
||||||
|
*/
|
||||||
|
private stopHealthMonitoring(): void {
|
||||||
|
if (this.healthCheckInterval) {
|
||||||
|
clearInterval(this.healthCheckInterval);
|
||||||
|
this.healthCheckInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle connection failure with exponential backoff
|
||||||
|
*/
|
||||||
|
private async handleConnectionFailure(error: any): Promise<void> {
|
||||||
|
this.health.consecutiveFailures++;
|
||||||
|
this.updateHealth();
|
||||||
|
this.notifyConnectionChange();
|
||||||
|
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts && !this.isReconnecting) {
|
||||||
|
await this.reconnectWithBackoff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconnect with exponential backoff
|
||||||
|
*/
|
||||||
|
private async reconnectWithBackoff(): Promise<void> {
|
||||||
|
if (this.isReconnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isReconnecting = true;
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
|
||||||
|
const backoffMs = Math.min(
|
||||||
|
this.reconnectBackoffMs * Math.pow(2, this.reconnectAttempts - 1),
|
||||||
|
this.maxBackoffMs
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${backoffMs}ms...`);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.connect();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error);
|
||||||
|
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
this.isReconnecting = false;
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
`Failed to reconnect to Task Master after ${this.maxReconnectAttempts} attempts. Please check your configuration and try manually reconnecting.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Try again
|
||||||
|
await this.reconnectWithBackoff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update health metrics
|
||||||
|
*/
|
||||||
|
private updateHealth(): void {
|
||||||
|
const status = this.getStatus();
|
||||||
|
this.health.isHealthy = status.isRunning && this.health.consecutiveFailures < 3;
|
||||||
|
|
||||||
|
if (this.startTime) {
|
||||||
|
this.health.uptime = Date.now() - this.startTime.getTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update average response time
|
||||||
|
*/
|
||||||
|
private updateAverageResponseTime(responseTime: number): void {
|
||||||
|
// Simple moving average calculation
|
||||||
|
if (this.health.averageResponseTime === 0) {
|
||||||
|
this.health.averageResponseTime = responseTime;
|
||||||
|
} else {
|
||||||
|
this.health.averageResponseTime = (this.health.averageResponseTime * 0.8) + (responseTime * 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log connection event
|
||||||
|
*/
|
||||||
|
private logEvent(event: ConnectionEvent): void {
|
||||||
|
this.connectionEvents.push(event);
|
||||||
|
|
||||||
|
// Keep only last 100 events
|
||||||
|
if (this.connectionEvents.length > 100) {
|
||||||
|
this.connectionEvents = this.connectionEvents.slice(-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onConnectionEvent) {
|
||||||
|
this.onConnectionEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify connection change
|
||||||
|
*/
|
||||||
|
private notifyConnectionChange(): void {
|
||||||
|
if (this.onConnectionChange) {
|
||||||
|
this.onConnectionChange(this.getStatus(), this.getHealth());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
787
apps/extension/src/utils/errorHandler.ts
Normal file
787
apps/extension/src/utils/errorHandler.ts
Normal file
@@ -0,0 +1,787 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { shouldShowNotification, getNotificationType, getToastDuration } 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: string = '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: string = '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: string = '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: string = '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: string = '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) {
|
||||||
|
console.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:
|
||||||
|
console.error(logMessage, errorDetails);
|
||||||
|
break;
|
||||||
|
case ErrorSeverity.MEDIUM:
|
||||||
|
console.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) {
|
||||||
|
console.error('Error in error listener:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup global error handlers
|
||||||
|
*/
|
||||||
|
private setupGlobalErrorHandlers(): void {
|
||||||
|
// Handle unhandled promise rejections
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
// Create a concrete error class for internal errors
|
||||||
|
class InternalError extends TaskMasterError {
|
||||||
|
constructor(message: string, code: string, severity: ErrorSeverity, context?: Record<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 };
|
||||||
|
}
|
||||||
335
apps/extension/src/utils/mcpClient.ts
Normal file
335
apps/extension/src/utils/mcpClient.ts
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export interface MCPConfig {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
cwd?: string;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.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
|
||||||
|
console.log(`Starting MCP server: ${this.config.command} ${this.config.args?.join(' ') || ''}`);
|
||||||
|
console.log('🔍 DEBUGGING: Transport config cwd:', this.config.cwd);
|
||||||
|
console.log('🔍 DEBUGGING: Process cwd before spawn:', process.cwd());
|
||||||
|
|
||||||
|
// Test if the target directory and .taskmaster exist
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
try {
|
||||||
|
const targetDir = this.config.cwd;
|
||||||
|
const taskmasterDir = path.join(targetDir, '.taskmaster');
|
||||||
|
const tasksFile = path.join(taskmasterDir, 'tasks', 'tasks.json');
|
||||||
|
|
||||||
|
console.log('🔍 DEBUGGING: Checking target directory:', targetDir, 'exists:', fs.existsSync(targetDir));
|
||||||
|
console.log('🔍 DEBUGGING: Checking .taskmaster dir:', taskmasterDir, 'exists:', fs.existsSync(taskmasterDir));
|
||||||
|
console.log('🔍 DEBUGGING: Checking tasks.json:', tasksFile, 'exists:', fs.existsSync(tasksFile));
|
||||||
|
|
||||||
|
if (fs.existsSync(tasksFile)) {
|
||||||
|
const stats = fs.statSync(tasksFile);
|
||||||
|
console.log('🔍 DEBUGGING: tasks.json size:', stats.size, 'bytes');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('🔍 DEBUGGING: Error checking filesystem:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transport = new StdioClientTransport({
|
||||||
|
command: this.config.command,
|
||||||
|
args: this.config.args || [],
|
||||||
|
cwd: this.config.cwd,
|
||||||
|
env: {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(process.env).filter(([, v]) => v !== undefined)
|
||||||
|
) as Record<string, string>,
|
||||||
|
...this.config.env,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🔍 DEBUGGING: Transport created, checking process...');
|
||||||
|
|
||||||
|
// Set up transport event handlers
|
||||||
|
this.transport.onerror = (error: Error) => {
|
||||||
|
console.error('❌ MCP transport error:', error);
|
||||||
|
console.error('Transport error details:', {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
code: (error as any).code,
|
||||||
|
errno: (error as any).errno,
|
||||||
|
syscall: (error as any).syscall
|
||||||
|
});
|
||||||
|
this.status = { isRunning: false, error: error.message };
|
||||||
|
vscode.window.showErrorMessage(`Task Master MCP transport error: ${error.message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.transport.onclose = () => {
|
||||||
|
console.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) => {
|
||||||
|
console.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)
|
||||||
|
console.log('🔄 Attempting MCP client connection...');
|
||||||
|
console.log('MCP config:', { command: this.config.command, args: this.config.args, cwd: this.config.cwd });
|
||||||
|
console.log('Current working directory:', process.cwd());
|
||||||
|
console.log('VS Code workspace folders:', vscode.workspace.workspaceFolders?.map(f => f.uri.fsPath));
|
||||||
|
|
||||||
|
// Check if process was created before connecting
|
||||||
|
if (this.transport && (this.transport as any).process) {
|
||||||
|
const proc = (this.transport as any).process;
|
||||||
|
console.log('📝 MCP server process PID:', proc.pid);
|
||||||
|
console.log('📝 Process working directory will be:', this.config.cwd);
|
||||||
|
|
||||||
|
proc.on('exit', (code: number, signal: string) => {
|
||||||
|
console.log(`🔚 MCP server process exited with code ${code}, signal ${signal}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
console.log('❌ Non-zero exit code indicates server failure');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', (error: Error) => {
|
||||||
|
console.log('❌ MCP server process error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to stderr to see server-side errors
|
||||||
|
if (proc.stderr) {
|
||||||
|
proc.stderr.on('data', (data: Buffer) => {
|
||||||
|
console.log('📥 MCP server stderr:', data.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to stdout for server messages
|
||||||
|
if (proc.stdout) {
|
||||||
|
proc.stdout.on('data', (data: Buffer) => {
|
||||||
|
console.log('📤 MCP server stdout:', data.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ No process found in transport before connection');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.client.connect(this.transport);
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
this.status = {
|
||||||
|
isRunning: true,
|
||||||
|
pid: this.transport.pid || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('MCP client connected successfully');
|
||||||
|
vscode.window.showInformationMessage('Task Master connected successfully');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to MCP server:', error);
|
||||||
|
this.status = {
|
||||||
|
isRunning: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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> {
|
||||||
|
console.log('Disconnecting from MCP server');
|
||||||
|
|
||||||
|
if (this.client) {
|
||||||
|
try {
|
||||||
|
await this.client.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error closing MCP client:', error);
|
||||||
|
}
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.transport) {
|
||||||
|
try {
|
||||||
|
await this.transport.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error closing MCP transport:', error);
|
||||||
|
}
|
||||||
|
this.transport = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = { isRunning: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the MCP client instance (if connected)
|
||||||
|
*/
|
||||||
|
getClient(): Client | null {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call an MCP tool
|
||||||
|
*/
|
||||||
|
async callTool(toolName: string, arguments_: Record<string, unknown>): Promise<any> {
|
||||||
|
if (!this.client) {
|
||||||
|
throw new Error('MCP client is not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.client.callTool({
|
||||||
|
name: toolName,
|
||||||
|
arguments: arguments_,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error calling MCP tool "${toolName}":`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the connection by calling a simple MCP tool
|
||||||
|
*/
|
||||||
|
async testConnection(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Try to list available tools as a connection test
|
||||||
|
if (!this.client) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.client.listTools();
|
||||||
|
console.log('Available MCP tools:', result.tools?.map(t => t.name) || []);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Connection test failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stderr stream from the transport (if available)
|
||||||
|
*/
|
||||||
|
getStderr(): NodeJS.ReadableStream | null {
|
||||||
|
const stderr = this.transport?.stderr;
|
||||||
|
return stderr ? (stderr as unknown as NodeJS.ReadableStream) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the process ID of the spawned server
|
||||||
|
*/
|
||||||
|
getPid(): number | null {
|
||||||
|
return this.transport?.pid || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create MCP configuration from VS Code settings
|
||||||
|
*/
|
||||||
|
export function createMCPConfigFromSettings(): MCPConfig {
|
||||||
|
console.log('🔍 DEBUGGING: createMCPConfigFromSettings called at', new Date().toISOString());
|
||||||
|
const config = vscode.workspace.getConfiguration('taskmaster');
|
||||||
|
|
||||||
|
let command = config.get<string>('mcp.command', 'npx');
|
||||||
|
const args = config.get<string[]>('mcp.args', ['-y', '--package=task-master-ai', 'task-master-ai']);
|
||||||
|
|
||||||
|
// Use proper VS Code workspace detection
|
||||||
|
const defaultCwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
|
||||||
|
const cwd = config.get<string>('mcp.cwd', defaultCwd);
|
||||||
|
const env = config.get<Record<string, string>>('mcp.env');
|
||||||
|
|
||||||
|
console.log('✅ Using workspace directory:', defaultCwd);
|
||||||
|
|
||||||
|
// If using default 'npx', try to find the full path on macOS/Linux
|
||||||
|
if (command === 'npx') {
|
||||||
|
const fs = require('fs');
|
||||||
|
const npxPaths = [
|
||||||
|
'/opt/homebrew/bin/npx', // Homebrew on Apple Silicon
|
||||||
|
'/usr/local/bin/npx', // Homebrew on Intel
|
||||||
|
'/usr/bin/npx', // System npm
|
||||||
|
'npx' // Final fallback to PATH
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of npxPaths) {
|
||||||
|
try {
|
||||||
|
if (path === 'npx' || fs.existsSync(path)) {
|
||||||
|
command = path;
|
||||||
|
console.log(`✅ Using npx at: ${path}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Continue to next path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
cwd: cwd || defaultCwd,
|
||||||
|
env
|
||||||
|
};
|
||||||
|
}
|
||||||
276
apps/extension/src/utils/notificationPreferences.ts
Normal file
276
apps/extension/src/utils/notificationPreferences.ts
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { ErrorCategory, ErrorSeverity, NotificationType } from './errorHandler';
|
||||||
|
|
||||||
|
export interface NotificationPreferences {
|
||||||
|
// Global notification toggles
|
||||||
|
enableToastNotifications: boolean;
|
||||||
|
enableVSCodeNotifications: boolean;
|
||||||
|
enableConsoleLogging: boolean;
|
||||||
|
|
||||||
|
// Toast notification settings
|
||||||
|
toastDuration: {
|
||||||
|
info: number;
|
||||||
|
warning: number;
|
||||||
|
error: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Category-based preferences
|
||||||
|
categoryPreferences: Record<ErrorCategory, {
|
||||||
|
showToUser: boolean;
|
||||||
|
notificationType: NotificationType;
|
||||||
|
logToConsole: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Severity-based preferences
|
||||||
|
severityPreferences: Record<ErrorSeverity, {
|
||||||
|
showToUser: boolean;
|
||||||
|
notificationType: NotificationType;
|
||||||
|
minToastDuration: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Advanced settings
|
||||||
|
maxToastCount: number;
|
||||||
|
enableErrorTracking: boolean;
|
||||||
|
enableDetailedErrorInfo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
console.log('Task Master Kanban notification preferences reset to defaults');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get category-based preferences with defaults
|
||||||
|
*/
|
||||||
|
private getCategoryPreferences(config: vscode.WorkspaceConfiguration): Record<ErrorCategory, { showToUser: boolean; notificationType: NotificationType; logToConsole: boolean }> {
|
||||||
|
const defaults = {
|
||||||
|
[ErrorCategory.MCP_CONNECTION]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.CONFIGURATION]: { showToUser: true, notificationType: NotificationType.VSCODE_WARNING, logToConsole: true },
|
||||||
|
[ErrorCategory.TASK_LOADING]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
|
||||||
|
[ErrorCategory.UI_RENDERING]: { showToUser: true, notificationType: NotificationType.TOAST_INFO, logToConsole: false },
|
||||||
|
[ErrorCategory.VALIDATION]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
|
||||||
|
[ErrorCategory.NETWORK]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
|
||||||
|
[ErrorCategory.INTERNAL]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.TASK_MASTER_API]: { showToUser: true, notificationType: NotificationType.TOAST_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.DATA_VALIDATION]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
|
||||||
|
[ErrorCategory.DATA_PARSING]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.TASK_DATA_CORRUPTION]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.VSCODE_API]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.WEBVIEW]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
|
||||||
|
[ErrorCategory.EXTENSION_HOST]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.USER_INTERACTION]: { showToUser: false, notificationType: NotificationType.CONSOLE_ONLY, logToConsole: true },
|
||||||
|
[ErrorCategory.DRAG_DROP]: { showToUser: true, notificationType: NotificationType.TOAST_INFO, logToConsole: false },
|
||||||
|
[ErrorCategory.COMPONENT_RENDER]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
|
||||||
|
[ErrorCategory.PERMISSION]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.FILE_SYSTEM]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
|
||||||
|
[ErrorCategory.UNKNOWN]: { showToUser: true, notificationType: NotificationType.VSCODE_WARNING, logToConsole: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow user overrides from settings
|
||||||
|
const userPreferences = config.get('notifications.categoryPreferences', {});
|
||||||
|
return { ...defaults, ...userPreferences };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get severity-based preferences with defaults
|
||||||
|
*/
|
||||||
|
private getSeverityPreferences(config: vscode.WorkspaceConfiguration): Record<ErrorSeverity, { showToUser: boolean; notificationType: NotificationType; minToastDuration: number }> {
|
||||||
|
const defaults = {
|
||||||
|
[ErrorSeverity.LOW]: { showToUser: true, notificationType: NotificationType.TOAST_INFO, minToastDuration: 3000 },
|
||||||
|
[ErrorSeverity.MEDIUM]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, minToastDuration: 5000 },
|
||||||
|
[ErrorSeverity.HIGH]: { showToUser: true, notificationType: NotificationType.VSCODE_WARNING, minToastDuration: 7000 },
|
||||||
|
[ErrorSeverity.CRITICAL]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, minToastDuration: 10000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow user overrides from settings
|
||||||
|
const userPreferences = config.get('notifications.severityPreferences', {});
|
||||||
|
return { ...defaults, ...userPreferences };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default notification type for severity
|
||||||
|
*/
|
||||||
|
private getDefaultNotificationType(severity: ErrorSeverity): NotificationType {
|
||||||
|
switch (severity) {
|
||||||
|
case ErrorSeverity.LOW:
|
||||||
|
return NotificationType.TOAST_INFO;
|
||||||
|
case ErrorSeverity.MEDIUM:
|
||||||
|
return NotificationType.TOAST_WARNING;
|
||||||
|
case ErrorSeverity.HIGH:
|
||||||
|
return NotificationType.VSCODE_WARNING;
|
||||||
|
case ErrorSeverity.CRITICAL:
|
||||||
|
return NotificationType.VSCODE_ERROR;
|
||||||
|
default:
|
||||||
|
return NotificationType.CONSOLE_ONLY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export convenience functions
|
||||||
|
export function getNotificationPreferences(): NotificationPreferences {
|
||||||
|
return NotificationPreferencesManager.getInstance().getPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
174
apps/extension/src/utils/taskFileReader.ts
Normal file
174
apps/extension/src/utils/taskFileReader.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export interface TaskFileData {
|
||||||
|
details?: string;
|
||||||
|
testStrategy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TasksJsonStructure {
|
||||||
|
[tagName: string]: {
|
||||||
|
tasks: TaskWithDetails[];
|
||||||
|
metadata: {
|
||||||
|
createdAt: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskWithDetails {
|
||||||
|
id: string | number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
dependencies?: (string | number)[];
|
||||||
|
details?: string;
|
||||||
|
testStrategy?: string;
|
||||||
|
subtasks?: TaskWithDetails[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads tasks.json file directly and extracts implementation details and test strategy
|
||||||
|
* @param taskId - The ID of the task to read (e.g., "1" or "1.2" for subtasks)
|
||||||
|
* @param tagName - The tag/context name (defaults to "master")
|
||||||
|
* @returns TaskFileData with details and testStrategy fields
|
||||||
|
*/
|
||||||
|
export async function readTaskFileData(taskId: string, tagName: string = 'master'): Promise<TaskFileData> {
|
||||||
|
try {
|
||||||
|
// Check if we're in a VS Code webview context
|
||||||
|
if (typeof window !== 'undefined' && (window as any).vscode) {
|
||||||
|
// Use VS Code API to read the file
|
||||||
|
const vscode = (window as any).vscode;
|
||||||
|
|
||||||
|
// Request file content from the extension
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const messageId = Date.now().toString();
|
||||||
|
|
||||||
|
// Listen for response
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.type === 'taskFileData' && message.messageId === messageId) {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
if (message.error) {
|
||||||
|
reject(new Error(message.error));
|
||||||
|
} else {
|
||||||
|
resolve(message.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
// Send request to extension
|
||||||
|
vscode.postMessage({
|
||||||
|
type: 'readTaskFileData',
|
||||||
|
messageId,
|
||||||
|
taskId,
|
||||||
|
tagName
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
reject(new Error('Timeout reading task file data'));
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback for non-VS Code environments
|
||||||
|
return { details: undefined, testStrategy: undefined };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading task file data:', error);
|
||||||
|
return { details: undefined, testStrategy: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a task by ID within a tasks array, supporting subtask notation (e.g., "1.2")
|
||||||
|
* @param tasks - Array of tasks to search
|
||||||
|
* @param taskId - ID to search for
|
||||||
|
* @returns The task object if found, undefined otherwise
|
||||||
|
*/
|
||||||
|
export function findTaskById(tasks: TaskWithDetails[], taskId: string): TaskWithDetails | undefined {
|
||||||
|
// Check if this is a subtask ID with dotted notation (e.g., "1.2")
|
||||||
|
if (taskId.includes('.')) {
|
||||||
|
const [parentId, subtaskId] = taskId.split('.');
|
||||||
|
console.log('🔍 Looking for subtask:', { parentId, subtaskId, taskId });
|
||||||
|
|
||||||
|
// Find the parent task first
|
||||||
|
const parentTask = tasks.find(task => String(task.id) === parentId);
|
||||||
|
if (!parentTask || !parentTask.subtasks) {
|
||||||
|
console.log('❌ Parent task not found or has no subtasks:', parentId);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Parent task found with', parentTask.subtasks.length, 'subtasks');
|
||||||
|
console.log('🔍 Subtask IDs in parent:', parentTask.subtasks.map(st => st.id));
|
||||||
|
|
||||||
|
// Find the subtask within the parent
|
||||||
|
const subtask = parentTask.subtasks.find(st => String(st.id) === subtaskId);
|
||||||
|
if (subtask) {
|
||||||
|
console.log('✅ Subtask found:', subtask.id);
|
||||||
|
} else {
|
||||||
|
console.log('❌ Subtask not found:', subtaskId);
|
||||||
|
}
|
||||||
|
return subtask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For regular task IDs (not dotted notation)
|
||||||
|
for (const task of tasks) {
|
||||||
|
// Convert both to strings for comparison to handle string vs number IDs
|
||||||
|
if (String(task.id) === String(taskId)) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses tasks.json content and extracts task file data (details and testStrategy only)
|
||||||
|
* @param content - Raw tasks.json content
|
||||||
|
* @param taskId - Task ID to find
|
||||||
|
* @param tagName - Tag name to use
|
||||||
|
* @param workspacePath - Path to workspace root (not used anymore but kept for compatibility)
|
||||||
|
* @returns TaskFileData with details and testStrategy only
|
||||||
|
*/
|
||||||
|
export function parseTaskFileData(content: string, taskId: string, tagName: string, workspacePath?: string): TaskFileData {
|
||||||
|
console.log('🔍 parseTaskFileData called with:', { taskId, tagName, contentLength: content.length });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tasksJson: TasksJsonStructure = JSON.parse(content);
|
||||||
|
console.log('📊 Available tags:', Object.keys(tasksJson));
|
||||||
|
|
||||||
|
// Get the tag data
|
||||||
|
const tagData = tasksJson[tagName];
|
||||||
|
if (!tagData || !tagData.tasks) {
|
||||||
|
console.log('❌ Tag not found or no tasks in tag:', tagName);
|
||||||
|
return { details: undefined, testStrategy: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Tag found with', tagData.tasks.length, 'tasks');
|
||||||
|
console.log('🔍 Available task IDs:', tagData.tasks.map(t => t.id));
|
||||||
|
|
||||||
|
// Find the task
|
||||||
|
const task = findTaskById(tagData.tasks, taskId);
|
||||||
|
if (!task) {
|
||||||
|
console.log('❌ Task not found:', taskId);
|
||||||
|
return { details: undefined, testStrategy: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Task found:', task.id);
|
||||||
|
console.log('📝 Task has details:', !!task.details, 'length:', task.details?.length);
|
||||||
|
console.log('🧪 Task has testStrategy:', !!task.testStrategy, 'length:', task.testStrategy?.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
details: task.details,
|
||||||
|
testStrategy: task.testStrategy
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error parsing tasks.json:', error);
|
||||||
|
return { details: undefined, testStrategy: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
1134
apps/extension/src/utils/taskMasterApi.ts
Normal file
1134
apps/extension/src/utils/taskMasterApi.ts
Normal file
File diff suppressed because it is too large
Load Diff
190
apps/extension/src/webview/index.css
Normal file
190
apps/extension/src/webview/index.css
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* shadcn/ui CSS variables */
|
||||||
|
@theme {
|
||||||
|
/* VS Code CSS variables will be injected here */
|
||||||
|
/* color-scheme: var(--vscode-theme-kind, light); */
|
||||||
|
|
||||||
|
/* shadcn/ui variables - adapted for VS Code */
|
||||||
|
--color-background: var(--vscode-editor-background);
|
||||||
|
--color-sidebar-background: var(--vscode-sideBar-background);
|
||||||
|
--color-foreground: var(--vscode-foreground);
|
||||||
|
--color-card: var(--vscode-editor-background);
|
||||||
|
--color-card-foreground: var(--vscode-foreground);
|
||||||
|
--color-popover: var(--vscode-editor-background);
|
||||||
|
--color-popover-foreground: var(--vscode-foreground);
|
||||||
|
--color-primary: var(--vscode-button-background);
|
||||||
|
--color-primary-foreground: var(--vscode-button-foreground);
|
||||||
|
--color-secondary: var(--vscode-button-secondaryBackground);
|
||||||
|
--color-secondary-foreground: var(--vscode-button-secondaryForeground);
|
||||||
|
--color-widget-background: var(--vscode-editorWidget-background);
|
||||||
|
--color-widget-border: var(--vscode-editorWidget-border);
|
||||||
|
--color-code-snippet-background: var(--vscode-textPreformat-background);
|
||||||
|
--color-code-snippet-text: var(--vscode-textPreformat-foreground);
|
||||||
|
--font-editor-font: var(--vscode-editor-font-family);
|
||||||
|
--font-editor-size: var(--vscode-editor-font-size);
|
||||||
|
--color-input-background: var(--vscode-input-background);
|
||||||
|
--color-input-foreground: var(--vscode-input-foreground);
|
||||||
|
--color-accent: var(--vscode-focusBorder);
|
||||||
|
--color-accent-foreground: var(--vscode-foreground);
|
||||||
|
--color-destructive: var(--vscode-errorForeground);
|
||||||
|
--color-destructive-foreground: var(--vscode-foreground);
|
||||||
|
--color-border: var(--vscode-panel-border);
|
||||||
|
--color-ring: var(--vscode-focusBorder);
|
||||||
|
--color-link: var(--vscode-editorLink-foreground);
|
||||||
|
--color-link-hover: var(--vscode-editorLink-activeForeground);
|
||||||
|
--color-textSeparator-foreground: var(--vscode-textSeparator-foreground);
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Reset body to match VS Code styles instead of Tailwind defaults */
|
||||||
|
@layer base {
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--vscode-editor-background) !important;
|
||||||
|
color: var(--vscode-foreground) !important;
|
||||||
|
font-family: var(--vscode-font-family) !important;
|
||||||
|
font-size: var(--vscode-font-size) !important;
|
||||||
|
font-weight: var(--vscode-font-weight) !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure root container takes full space */
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override any conflicting Tailwind defaults for VS Code integration */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure buttons and inputs use VS Code styling */
|
||||||
|
button, input, select, textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced scrollbar styling for Kanban board */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2));
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--vscode-scrollbarSlider-activeBackground, rgba(255, 255, 255, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban specific styles */
|
||||||
|
@layer components {
|
||||||
|
.kanban-container {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scrolling for better UX */
|
||||||
|
.kanban-container {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper touch scrolling on mobile */
|
||||||
|
.kanban-container {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add subtle shadow for depth */
|
||||||
|
.kanban-column {
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced scrolling for column content areas */
|
||||||
|
.kanban-column > div[style*="overflow-y"] {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2))
|
||||||
|
var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card hover effects */
|
||||||
|
.kanban-card {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus indicators for accessibility */
|
||||||
|
.kanban-card:focus-visible {
|
||||||
|
outline: 2px solid var(--vscode-focusBorder);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Line clamp utility for text truncation */
|
||||||
|
@layer utilities {
|
||||||
|
.line-clamp-2 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-3 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar utilities */
|
||||||
|
.scrollbar-thin {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-track-transparent {
|
||||||
|
scrollbar-color: var(--vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2)) transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode adjustments */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
1462
apps/extension/src/webview/index.tsx
Normal file
1462
apps/extension/src/webview/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,113 +1,35 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
"module": "ESNext",
|
||||||
|
"target": "ES2022",
|
||||||
/* Projects */
|
"outDir": "out",
|
||||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
"lib": [
|
||||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
"ES2022",
|
||||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
"DOM"
|
||||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
],
|
||||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
"sourceMap": true,
|
||||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
"rootDir": "src",
|
||||||
|
"strict": true, /* enable all strict type-checking options */
|
||||||
/* Language and Environment */
|
"moduleResolution": "Node",
|
||||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
"esModuleInterop": true,
|
||||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
"skipLibCheck": true,
|
||||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
"forceConsistentCasingInFileNames": true,
|
||||||
// "libReplacement": true, /* Enable lib replacement. */
|
"jsx": "react-jsx",
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
"allowSyntheticDefaultImports": true,
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
"resolveJsonModule": true,
|
||||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
"declaration": false,
|
||||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
"declarationMap": false,
|
||||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
"baseUrl": ".",
|
||||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
"paths": {
|
||||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
"@/*": ["./src/*"],
|
||||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
"@/components/*": ["./src/components/*"],
|
||||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
"@/lib/*": ["./src/lib/*"]
|
||||||
|
}
|
||||||
/* Modules */
|
},
|
||||||
"module": "commonjs" /* Specify what module code is generated. */,
|
"exclude": [
|
||||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
"node_modules",
|
||||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
".vscode-test",
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
"out",
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
"dist"
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
]
|
||||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
|
||||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
||||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
|
||||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
|
||||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
|
||||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
|
||||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
|
||||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
|
||||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
|
||||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
|
||||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
|
||||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
|
||||||
|
|
||||||
/* JavaScript Support */
|
|
||||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
|
||||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
|
||||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
|
||||||
|
|
||||||
/* Emit */
|
|
||||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
|
||||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
|
||||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
|
||||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
|
||||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
|
||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
|
||||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
|
||||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
|
||||||
// "removeComments": true, /* Disable emitting comments. */
|
|
||||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
|
||||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
|
||||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
|
||||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
|
||||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
|
||||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
|
||||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
|
||||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
|
||||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
|
||||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
|
||||||
|
|
||||||
/* Interop Constraints */
|
|
||||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
|
||||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
|
||||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
|
||||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
|
||||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
|
||||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
|
||||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
|
||||||
|
|
||||||
/* Type Checking */
|
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
|
||||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
|
||||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
|
||||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
|
||||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
|
||||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
|
||||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
|
||||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
|
||||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
|
||||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
|
||||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
|
||||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
|
||||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
|
||||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
|
||||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
|
||||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
|
||||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
|
||||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
|
||||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
|
||||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
|
||||||
|
|
||||||
/* Completeness */
|
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
7855
package-lock.json
generated
7855
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user