diff --git a/.biomeignore b/.biomeignore index 5353a05..b0b03d3 100644 --- a/.biomeignore +++ b/.biomeignore @@ -1,18 +1,18 @@ -# Build output -out/ -dist/ - -# Dependencies -node_modules/ - -# Type definitions -**/*.d.ts - -# Config files -webpack.config.js - -# VSCode settings -.vscode/ - -# Large files -src/encoder.json +# Build output +out/ +dist/ + +# Dependencies +node_modules/ + +# Type definitions +**/*.d.ts + +# Config files +webpack.config.js + +# VSCode settings +.vscode/ + +# Large files +src/encoder.json diff --git a/.github/RELEASE.md b/.github/RELEASE.md new file mode 100644 index 0000000..aff5140 --- /dev/null +++ b/.github/RELEASE.md @@ -0,0 +1,299 @@ +# Release Process Documentation + +## Overview + +This project uses an automated GitHub Actions workflow for releasing the VS Code extension. The workflow handles version bumping, building, testing, publishing to the marketplace, and creating GitHub releases. + +## Prerequisites + +### Required Secrets + +You need to configure the following secret in your GitHub repository: + +1. **`VSCE_PAT`** - Personal Access Token for VS Code Marketplace + - Go to [Azure DevOps](https://dev.azure.com/) + - Create a new organization or use existing one + - Navigate to User Settings → Personal Access Tokens + - Create a new token with the following: + - **Name**: VS Code Marketplace Publishing + - **Organization**: All accessible organizations + - **Expiration**: Custom (1 year recommended) + - **Scopes**: + - Marketplace → **Manage** (required) + - Copy the token and add it to GitHub: + - Repository Settings → Secrets and variables → Actions + - New repository secret: `VSCE_PAT` + +## Automated Release Workflow + +### Trigger + +The release workflow is triggered manually via GitHub Actions: + +1. Go to **Actions** tab in your repository +2. Select **Release Extension** workflow +3. Click **Run workflow** +4. Choose version bump type: + - **auto** (default): Automatically determines version bump based on commits + - **major**: Breaking changes (1.0.0 → 2.0.0) + - **minor**: New features (1.1.0 → 1.2.0) + - **patch**: Bug fixes (1.1.0 → 1.1.1) + +### What the Workflow Does + +#### ✅ 1. Version Detection & Validation + +- Reads current version from `package.json` +- Checks if version tag already exists in Git +- Prevents duplicate releases + +#### ✅ 2. Commit Analysis (Auto mode) + +- Analyzes commits since last release +- Determines version bump based on conventional commits: + - `feat!:` or `BREAKING CHANGE:` → **major** + - `feat:` → **minor** + - `fix:`, `chore:`, `docs:` → **patch** + +#### ✅ 3. Version Bumping + +- Automatically bumps version if tag exists +- Updates `package.json` and `package-lock.json` +- Commits changes with message: `chore: bump version to X.Y.Z` +- Pushes version bump commit + +#### ✅ 4. Build & Test + +- Runs TypeScript type checking (`npm run type-check`) +- Runs linter (`npm run lint:check`) +- Compiles extension (`npm run compile`) +- Runs test suite (`npm run test`) + +#### ✅ 5. Package Extension + +- Packages extension as `.vsix` file +- Names file: `diffy-explain-ai-X.Y.Z.vsix` + +#### ✅ 6. Publish to Marketplace + +- Publishes extension to VS Code Marketplace +- Uses `VSCE_PAT` secret for authentication +- Updates existing extension listing + +#### ✅ 7. Create Git Tag + +- Creates annotated Git tag: `vX.Y.Z` +- Pushes tag to repository + +#### ✅ 8. Generate Release Notes + +- Automatically categorizes commits: + - ✨ **Features**: `feat:` commits + - 🐛 **Bug Fixes**: `fix:` commits + - 📝 **Documentation**: `docs:` commits + - 🔧 **Maintenance**: `chore:`, `build:`, `ci:` commits +- Includes commit hashes for traceability + +#### ✅ 9. Create GitHub Release + +- Creates GitHub Release with tag +- Includes auto-generated release notes +- Attaches `.vsix` file as release asset +- Not marked as draft or prerelease + +#### ✅ 10. Upload Artifacts + +- Uploads `.vsix` file as workflow artifact +- Retention: 90 days +- Available for download from workflow run + +## Version Bump Examples + +### Automatic Version Bumping + +**Scenario 1**: Bug fixes and chores + +```bash +git log v1.0.0..HEAD +# - fix: resolve OpenAI API timeout +# - chore: update dependencies +# - docs: improve README + +Result: 1.0.0 → 1.0.1 (patch) +``` + +**Scenario 2**: New features + +```bash +git log v1.1.0..HEAD +# - feat: add custom commit templates +# - fix: handle empty diffs + +Result: 1.1.0 → 1.2.0 (minor) +``` + +**Scenario 3**: Breaking changes + +```bash +git log v2.0.0..HEAD +# - feat!: redesign AI service interface +# - BREAKING CHANGE: remove deprecated methods + +Result: 2.0.0 → 3.0.0 (major) +``` + +### Manual Version Bumping + +You can override automatic detection: + +1. **Patch Release**: Bug fixes only + - Select `patch` in workflow input + - 1.1.0 → 1.1.1 + +2. **Minor Release**: New features + - Select `minor` in workflow input + - 1.1.0 → 1.2.0 + +3. **Major Release**: Breaking changes + - Select `major` in workflow input + - 1.1.0 → 2.0.0 + +## Manual Release Process + +If you need to release manually: + +### 1. Update Version + +```bash +npm version patch # or minor, major +``` + +### 2. Build Extension + +```bash +npm run type-check +npm run lint +npm run compile +``` + +### 3. Package Extension + +```bash +npm install -g @vscode/vsce +vsce package +``` + +### 4. Publish to Marketplace + +```bash +vsce publish -p YOUR_PERSONAL_ACCESS_TOKEN +``` + +### 5. Create GitHub Release + +```bash +git tag -a v1.1.0 -m "Release v1.1.0" +git push origin v1.1.0 + +# Then create release on GitHub with VSIX file +``` + +## Troubleshooting + +### Error: "Tag already exists" + +The workflow checks if a tag exists and automatically bumps the version. If you see this error: + +1. Check existing tags: `git tag -l` +2. Delete tag if needed: `git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z` +3. Re-run workflow + +### Error: "VSCE_PAT is not set" + +1. Verify secret exists in repository settings +2. Ensure it's named exactly `VSCE_PAT` +3. Generate new token if expired + +### Error: "Extension validation failed" + +1. Check `package.json` for required fields +2. Ensure `engines.vscode` matches `@types/vscode` +3. Verify all files referenced in `package.json` exist + +### Tests Failed + +Tests failures won't block the release (marked as `continue-on-error`), but you should: + +1. Review test failures in workflow logs +2. Fix failing tests +3. Consider making tests blocking by removing `continue-on-error: true` + +## Best Practices + +### Commit Messages + +Use conventional commits for automatic version bumping: + +```bash +# Features (minor bump) +git commit -m "feat: add new AI model support" +git commit -m "feat(git): implement file filtering" + +# Bug fixes (patch bump) +git commit -m "fix: resolve memory leak in diff parser" +git commit -m "fix(ai): handle API rate limiting" + +# Breaking changes (major bump) +git commit -m "feat!: redesign configuration API" +git commit -m "feat: new config format + +BREAKING CHANGE: Configuration schema has changed" + +# Other types (patch bump) +git commit -m "docs: update installation guide" +git commit -m "chore: update dependencies" +git commit -m "style: format code with Biome" +``` + +### Release Cadence + +- **Patch releases**: Weekly or as needed for bug fixes +- **Minor releases**: Monthly for new features +- **Major releases**: Quarterly or when breaking changes necessary + +### Pre-Release Checklist + +Before triggering a release: + +1. ✅ Ensure all tests pass locally +2. ✅ Update CHANGELOG.md if maintained +3. ✅ Review open issues and PRs +4. ✅ Test extension locally with `vsce package` +5. ✅ Verify documentation is up to date + +## Monitoring + +### After Release + +Check the following: + +1. **Marketplace**: Extension appears at +2. **GitHub Release**: Release created with VSIX file attached +3. **Git Tag**: Tag pushed to repository +4. **Workflow Artifacts**: VSIX available for 90 days + +### Rollback + +If you need to rollback a release: + +1. Unpublish from marketplace (contact VS Code team) +2. Delete GitHub release +3. Delete Git tag: `git push origin :refs/tags/vX.Y.Z` +4. Revert version in `package.json` + +## Resources + +- [Publishing Extensions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) +- [Conventional Commits](https://www.conventionalcommits.org/) +- [Semantic Versioning](https://semver.org/) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index efd624c..674d701 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,28 +1,31 @@ -name: Node.js CI - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [16.x, 18.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: "npm" - - run: npm install - - run: npm run type-check - - run: npm run lint - - run: npm run compile +name: Node.js CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x, 18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - run: npm install + - run: npm run type-check + - run: npm run lint + - run: npm run compile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ca40f00 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,236 @@ +name: Release Extension + +on: + workflow_dispatch: + inputs: + version-bump: + description: "Version bump type (auto/major/minor/patch)" + required: false + default: "auto" + type: choice + options: + - auto + - major + - minor + - patch + +permissions: + contents: write + pull-requests: read + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for version detection + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18.x" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Get current version + id: current-version + run: | + CURRENT_VERSION=$(node -p "require('./package.json').version") + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "Current version: $CURRENT_VERSION" + + - name: Check if version tag exists + id: check-tag + run: | + if git rev-parse "v${{ steps.current-version.outputs.version }}" >/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Tag v${{ steps.current-version.outputs.version }} already exists" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Tag v${{ steps.current-version.outputs.version }} does not exist" + fi + + - name: Analyze commits for version bump + id: analyze-commits + if: steps.check-tag.outputs.exists == 'true' || github.event.inputs.version-bump == 'auto' + run: | + # Get commits since last tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -z "$LAST_TAG" ]; then + echo "No previous tags found, defaulting to patch" + echo "bump=patch" >> $GITHUB_OUTPUT + exit 0 + fi + + COMMITS=$(git log $LAST_TAG..HEAD --pretty=format:"%s") + + if echo "$COMMITS" | grep -qE "^(feat|feature)(\(.+\))?!:|^BREAKING CHANGE:|^[a-z]+(\(.+\))?!:"; then + echo "bump=major" >> $GITHUB_OUTPUT + echo "Detected BREAKING CHANGE - major version bump" + elif echo "$COMMITS" | grep -qE "^(feat|feature)(\(.+\))?:"; then + echo "bump=minor" >> $GITHUB_OUTPUT + echo "Detected feature - minor version bump" + else + echo "bump=patch" >> $GITHUB_OUTPUT + echo "Detected fixes/chores - patch version bump" + fi + + - name: Determine version bump type + id: version-bump + run: | + if [ "${{ github.event.inputs.version-bump }}" != "auto" ] && [ -n "${{ github.event.inputs.version-bump }}" ]; then + echo "type=${{ github.event.inputs.version-bump }}" >> $GITHUB_OUTPUT + echo "Using manual version bump: ${{ github.event.inputs.version-bump }}" + else + echo "type=${{ steps.analyze-commits.outputs.bump }}" >> $GITHUB_OUTPUT + echo "Using automatic version bump: ${{ steps.analyze-commits.outputs.bump }}" + fi + + - name: Bump version + id: bump-version + if: steps.check-tag.outputs.exists == 'true' + run: | + npm version ${{ steps.version-bump.outputs.type }} --no-git-tag-version + NEW_VERSION=$(node -p "require('./package.json').version") + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "Bumped version to: $NEW_VERSION" + + - name: Set final version + id: final-version + run: | + if [ "${{ steps.check-tag.outputs.exists }}" == "true" ]; then + echo "version=${{ steps.bump-version.outputs.version }}" >> $GITHUB_OUTPUT + else + echo "version=${{ steps.current-version.outputs.version }}" >> $GITHUB_OUTPUT + fi + + - name: Commit version changes + if: steps.check-tag.outputs.exists == 'true' + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add package.json package-lock.json + git commit -m "chore: bump version to ${{ steps.final-version.outputs.version }}" + git push + + - name: Run type check + run: npm run type-check + + - name: Run linter + run: npm run lint:check + + - name: Build extension + run: npm run compile + + - name: Run tests + run: npm run test + continue-on-error: true # Don't fail release if tests fail + + - name: Install vsce + run: npm install -g @vscode/vsce + + - name: Package extension + run: vsce package -o diffy-explain-ai-${{ steps.final-version.outputs.version }}.vsix + + - name: Publish to VS Code Marketplace + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + run: vsce publish -p $VSCE_PAT --packagePath diffy-explain-ai-${{ steps.final-version.outputs.version }}.vsix + + - name: Create Git tag + run: | + git tag -a "v${{ steps.final-version.outputs.version }}" -m "Release v${{ steps.final-version.outputs.version }}" + git push origin "v${{ steps.final-version.outputs.version }}" + + - name: Generate release notes + id: release-notes + run: | + LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + + if [ -z "$LAST_TAG" ]; then + CHANGES=$(git log --pretty=format:"- %s (%h)" --no-merges) + else + CHANGES=$(git log $LAST_TAG..HEAD --pretty=format:"- %s (%h)" --no-merges) + fi + + # Categorize changes + FEATURES=$(echo "$CHANGES" | grep -E "^- (feat|feature)" || echo "") + FIXES=$(echo "$CHANGES" | grep -E "^- fix" || echo "") + DOCS=$(echo "$CHANGES" | grep -E "^- docs" || echo "") + CHORES=$(echo "$CHANGES" | grep -E "^- (chore|build|ci)" || echo "") + OTHERS=$(echo "$CHANGES" | grep -vE "^- (feat|feature|fix|docs|chore|build|ci)" || echo "") + + # Build release notes + NOTES="## What's Changed" + + if [ -n "$FEATURES" ]; then + NOTES="$NOTES\n\n### ✨ Features\n$FEATURES" + fi + + if [ -n "$FIXES" ]; then + NOTES="$NOTES\n\n### 🐛 Bug Fixes\n$FIXES" + fi + + if [ -n "$DOCS" ]; then + NOTES="$NOTES\n\n### 📝 Documentation\n$DOCS" + fi + + if [ -n "$CHORES" ]; then + NOTES="$NOTES\n\n### 🔧 Maintenance\n$CHORES" + fi + + if [ -n "$OTHERS" ]; then + NOTES="$NOTES\n\n### Other Changes\n$OTHERS" + fi + + # Save to file for GitHub Release + echo -e "$NOTES" > release-notes.md + + echo "Generated release notes:" + cat release-notes.md + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.final-version.outputs.version }} + name: Release v${{ steps.final-version.outputs.version }} + body_path: release-notes.md + files: diffy-explain-ai-${{ steps.final-version.outputs.version }}.vsix + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload VSIX artifact + uses: actions/upload-artifact@v4 + with: + name: diffy-explain-ai-${{ steps.final-version.outputs.version }} + path: diffy-explain-ai-${{ steps.final-version.outputs.version }}.vsix + retention-days: 90 + if-no-files-found: error + + - name: Release summary + run: | + echo "## 🎉 Release Complete!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: v${{ steps.final-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Extension Package**: diffy-explain-ai-${{ steps.final-version.outputs.version }}.vsix" >> $GITHUB_STEP_SUMMARY + echo "- **Marketplace**: Published ✅" >> $GITHUB_STEP_SUMMARY + echo "- **GitHub Release**: Created ✅" >> $GITHUB_STEP_SUMMARY + echo "- **Git Tag**: v${{ steps.final-version.outputs.version }} ✅" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📦 Install" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "# From VS Code Marketplace" >> $GITHUB_STEP_SUMMARY + echo "code --install-extension hitclaw.diffy-explain-ai" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "# Or download VSIX from GitHub Release" >> $GITHUB_STEP_SUMMARY + echo "code --install-extension diffy-explain-ai-${{ steps.final-version.outputs.version }}.vsix" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 0b60dfa..bddd37a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,38 @@ dist node_modules .vscode-test/ *.vsix + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Editor directories and files +.idea +*.swp +*.swo +*~ +.vscode/* +!.vscode/extensions.json +!.vscode/launch.json +!.vscode/settings.json +!.vscode/tasks.json diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 9e26dfe..0000000 --- a/.prettierrc +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 64d7097..19772a1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,25 +9,16 @@ "name": "Run Extension", "type": "extensionHost", "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - ], - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ], + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "${defaultBuildTask}" }, { "name": "Run Extension (Disable Other Extentions)", "type": "extensionHost", "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--disable-extensions" - ], - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ], + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-extensions"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "${defaultBuildTask}" }, { @@ -38,11 +29,8 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "${workspaceFolder}/dist/**/*.js" - ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "tasks: watch-tests" } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b66808..2e44d85 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,8 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", - "cSpell.words": ["Diffy", "GCMG"] + "cSpell.words": ["Diffy", "GCMG"], + // Use Biome as the default formatter + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c2ab68a..9e3300b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,40 +1,37 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "watch", - "problemMatcher": "$ts-webpack-watch", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "watchers" - }, - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "type": "npm", - "script": "watch-tests", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "watchers" - }, - "group": "build" - }, - { - "label": "tasks: watch-tests", - "dependsOn": [ - "npm: watch", - "npm: watch-tests" - ], - "problemMatcher": [] - } - ] + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$ts-webpack-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "type": "npm", + "script": "watch-tests", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": "build" + }, + { + "label": "tasks: watch-tests", + "dependsOn": ["npm: watch", "npm: watch-tests"], + "problemMatcher": [] + } + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index ec694de..3c3a713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,43 +1,70 @@ -# Change Log - -All notable changes to the "diffy-explain-ai" 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 - -## [0.0.6] - 2022-12-24 - -### Added - -- Generate Commit Message. -- Explain Changes in Natural Language. -- Directly Generate Commit Message to VScode git commit input box. Copy to Clipboard - -## [0.0.7] - 2022-12-24 - -### Fixed - -- Commit Message Generation Improved - -## [0.0.8] - 2022-12-24 - -### Changes - -- Suggested Message as Newline Instead of Space - -## [0.0.9] - 2022-12-24 - -### Added - -- Explain and Preview Changes of Staged Files -### Changes - -- Bug Fixes - -## [0.0.10] - 2022-12-24 -### Changes - +# Change Log + +All notable changes to the "diffy-explain-ai" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [1.1.0] - 2025-11-03 + +### Added + +- **VS Code Language Model API Integration**: Support for GitHub Copilot as an AI service provider +- New configuration option `aiServiceProvider` to choose between OpenAI and VS Code LM +- New configuration option `vscodeLmModel` to select between `copilot-gpt-4o` and `copilot-gpt-3.5-turbo` +- Dual AI provider architecture allowing users to switch between OpenAI API and GitHub Copilot + +### Changed + +- Updated VS Code engine requirement to ^1.90.0 for Language Model API support +- Improved singleton pattern implementation in service classes +- Modernized imports with `node:` protocol for Node.js built-in modules +- Enhanced type safety with better TypeScript type definitions + +### Fixed + +- Fixed singleton constructor patterns in OpenAiService, WorkspaceService, and VsCodeLlmService +- Fixed confusing void type in EventEmitter type definitions +- Removed unused variables and parameters warnings + +### Documentation + +- Updated README with instructions for both OpenAI and GitHub Copilot setup +- Added comprehensive configuration guide for dual AI provider support + +## [Unreleased] + +- Initial release + +## [0.0.6] - 2022-12-24 + +### Added + +- Generate Commit Message. +- Explain Changes in Natural Language. +- Directly Generate Commit Message to VScode git commit input box. Copy to Clipboard + +## [0.0.7] - 2022-12-24 + +### Fixed + +- Commit Message Generation Improved + +## [0.0.8] - 2022-12-24 + +### Changes + +- Suggested Message as Newline Instead of Space + +## [0.0.9] - 2022-12-24 + +### Added + +- Explain and Preview Changes of Staged Files +### Changes + +- Bug Fixes + +## [0.0.10] - 2022-12-24 +### Changes + - Bug Fixes \ No newline at end of file diff --git a/README.md b/README.md index 6502e18..b0a574d 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,152 @@ -

-
- DIFFY -
- DIFFY COMMIT AI - Generate Your Commit Message or Explains the Changes -
-

- -

Generate Commit Message the changed code using git diff and OpenAI in natural language .

-
-

- - - Visual Studio Marketplace Downloads - - - CI - - - visual-studio marketplace - -

- -> Generate Commit   - -![screenshot](https://raw.githubusercontent.com/Hi7cl4w/diffy-explain-ai/main/images/generate_commit.gif) - -> Explain and Preview Changes of Staged Files   - -![screenshot](https://raw.githubusercontent.com/Hi7cl4w/diffy-explain-ai/main/images/explain_and_preview.png) - -## Key Features - -- Generate Commit Message. -- Explain Changes in Natural Language. -- Directly Generate Commit Message to VScode git commit input box. -- Copy to clipboard - -## Configure - -Go to Settings > Diffy - Explains Git Changes > Enter Your Api Key from OpenAI [Go to API Key Page](https://beta.openai.com/account/api-keys) - -## Commands - -- `diffy-explain-ai.generateCommitMessage` : Generate Commit Message to Commit Input Box -- `diffy-explain-ai.explainDiffClipboard` : Explain Changes and Copy to Clipboard -- `diffy-explain-ai.generateCommitMessageClipboard` : Generate Commit Message and Copy to Clipboard -- `diffy-explain-ai.explainAndPreview` : Explain and Preview Changes of Staged Files - -## Support - -[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/manukn) - -## License - -MIT - ---- - -> GitHub [@Hi7cl4w](https://github.com/Hi7cl4w)   +

+
+ DIFFY +
+ DIFFY COMMIT AI - Generate Your Commit Message or Explains the Changes +
+

+ +

Generate Commit Message for the changed code using git diff and AI (OpenAI or GitHub Copilot) in natural language.

+
+

+ + + Visual Studio Marketplace Downloads + + + CI + + + visual-studio marketplace + +

+ +> Generate Commit   + +![screenshot](https://raw.githubusercontent.com/Hi7cl4w/diffy-explain-ai/main/images/generate_commit.gif) + +> Explain and Preview Changes of Staged Files   + +![screenshot](https://raw.githubusercontent.com/Hi7cl4w/diffy-explain-ai/main/images/explain_and_preview.png) + +## Key Features + +- Generate Commit Message using **OpenAI** or **VS Code Language Models (GitHub Copilot)**. +- **Intelligent Code Indexing**: Automatically filters out irrelevant files (lock files, images, etc.) to improve AI analysis quality and reduce token usage. +- **Customizable Commit Formats**: Choose between Conventional Commits or Gitmoji styles. +- **Configurable Message Length**: Set maximum subject line length (50-200 characters). +- **Optional Detailed Bodies**: Include explanatory bullet points in commit messages. +- Explain Changes in Natural Language. +- Directly Generate Commit Message to VScode git commit input box. +- Copy to clipboard +- Choose between OpenAI API or GitHub Copilot as your AI provider + +## Configure + +### Option 1: Using OpenAI (Default) + +1. Go to Settings > Diffy - Explains Git Changes +2. Set **AI Service Provider** to `openai` +3. Enter Your API Key from OpenAI [Go to API Key Page](https://beta.openai.com/account/api-keys) + +### Option 2: Using VS Code Language Models (GitHub Copilot) + +1. Go to Settings > Diffy - Explains Git Changes +2. Set **AI Service Provider** to `vscode-lm` +3. Ensure GitHub Copilot is installed and you are signed in +4. Optionally select your preferred model: + - `copilot-gpt-4o` (default, recommended) + - `copilot-gpt-3.5-turbo` + +**Note:** Using VS Code Language Models requires an active GitHub Copilot subscription. + +## Advanced Configuration + +### Commit Message Customization + +#### Commit Format Style +Choose between two popular commit message formats: +- **Conventional Commits** (default): `feat: add user authentication` +- **Gitmoji**: `✨ feat: add user authentication` + +Set in: `Settings > Diffy > Commit Message Type` + +#### Include Commit Body +Enable detailed commit messages with explanatory bullet points: +``` +feat: add user authentication + +- Implement JWT token generation +- Add login and registration endpoints +- Create user model and database schema +``` + +Set in: `Settings > Diffy > Include Commit Body` + +#### Maximum Subject Line Length +Configure the maximum character length for commit subjects (default: 72) +- **50**: Strict GitHub standard +- **72**: Common standard (recommended) +- **100**: Relaxed limit + +Set in: `Settings > Diffy > Max Commit Message Length` + +### File Filtering + +Exclude specific files or patterns from AI analysis to improve quality and reduce token usage: + +**Default exclusions:** +- Lock files: `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` +- Images: `*.jpg`, `*.png`, `*.gif`, `*.svg`, `*.ico` +- Fonts: `*.woff`, `*.woff2`, `*.ttf`, `*.eot` + +**Add custom exclusions:** +Set in: `Settings > Diffy > Exclude Files From Diff` + +Example patterns: +- `*.min.js` - Exclude minified JavaScript +- `dist/**` - Exclude build directory +- `**/*.log` - Exclude all log files + +### Custom AI Instructions + +Override the default prompt with your own instructions for commit message generation: + +Set in: `Settings > Diffy > AI Instructions` + +Example: +``` +Follow my team's commit convention: +- Use JIRA ticket numbers in scope +- Always include "BREAKING CHANGE" footer when applicable +- Mention affected services +``` + +## Commands + +- `diffy-explain-ai.generateCommitMessage` : Generate Commit Message to Commit Input Box +- `diffy-explain-ai.explainDiffClipboard` : Explain Changes and Copy to Clipboard +- `diffy-explain-ai.generateCommitMessageClipboard` : Generate Commit Message and Copy to Clipboard +- `diffy-explain-ai.explainAndPreview` : Explain and Preview Changes of Staged Files + +## Contributing & Development + +For contributors and maintainers: + +- **Release Process**: See [.github/RELEASE.md](.github/RELEASE.md) for detailed information about our automated release workflow +- **CI/CD**: We use GitHub Actions for continuous integration and automated releases +- **Code Quality**: All code must pass TypeScript type checking, Biome linting, and formatting before merging + +## Support + +[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/manukn) + +## License + +MIT + +--- + +> GitHub [@Hi7cl4w](https://github.com/Hi7cl4w)   diff --git a/package.json b/package.json index 10bbbc7..0bba088 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,14 @@ "name": "diffy-explain-ai", "displayName": "Diffy Commit AI - Generate Your Commit Message", "description": "Generate Commit Message for You or Explains The Changed Code Using Git Diff And OpenAi In Natural Language", - "version": "1.0.17", + "version": "1.1.0", "publisher": "hitclaw", "engines": { - "vscode": "^1.90.0" + "vscode": "^1.105.0" }, "categories": [ "SCM Providers" ], - "activationEvents": [ - "onCommand:diffy-explain-ai.generateCommitMessage" - ], "repository": { "url": "https://github.com/Hi7cl4w/diffy-explain-ai.git" }, @@ -103,11 +100,29 @@ "diffy-explain-ai.vscodeLmModel": { "type": "string", "enum": [ + "auto", "copilot-gpt-4o", - "copilot-gpt-3.5-turbo" + "copilot-gpt-4", + "copilot-gpt-4-turbo", + "copilot-gpt-3.5-turbo", + "copilot-gpt-3.5", + "copilot-o1", + "copilot-o1-mini", + "copilot-o1-preview" + ], + "enumDescriptions": [ + "Automatically selects the best available model (recommended)", + "GitHub Copilot GPT-4o model - Latest and most capable", + "GitHub Copilot GPT-4 model - High capability model", + "GitHub Copilot GPT-4 Turbo model - Fast and capable", + "GitHub Copilot GPT-3.5 Turbo model - Fast and efficient", + "GitHub Copilot GPT-3.5 model - Standard capability", + "GitHub Copilot o1 model - Advanced reasoning model", + "GitHub Copilot o1-mini model - Compact reasoning model", + "GitHub Copilot o1-preview model - Preview of o1 capabilities" ], - "default": "copilot-gpt-4o", - "markdownDescription": "Select the VS Code Language Model to use when `aiServiceProvider` is set to `vscode-lm`. Requires GitHub Copilot subscription." + "default": "auto", + "markdownDescription": "Select the VS Code Language Model to use when `aiServiceProvider` is set to `vscode-lm`. Requires GitHub Copilot subscription. \n\nAvailable models may vary based on your Copilot subscription tier:\n\n- **auto**: Automatically selects the best available model\n- **copilot-gpt-4o**: Latest GPT-4o model (recommended for most users)\n- **copilot-gpt-4**: GPT-4 model with high reasoning capabilities\n- **copilot-gpt-4-turbo**: Fast GPT-4 Turbo model\n- **copilot-gpt-3.5-turbo**: Fast and efficient GPT-3.5 Turbo\n- **copilot-gpt-3.5**: Standard GPT-3.5 model\n- **copilot-o1**: Advanced o1 reasoning model (may require higher tier)\n- **copilot-o1-mini**: Compact o1-mini model\n- **copilot-o1-preview**: Preview access to o1 capabilities" }, "diffy-explain-ai.proxyUrl": { "type": "string", @@ -136,6 +151,59 @@ "diffy-explain-ai.aiInstructions": { "type": "string", "default": "Analyze the provided git diff --staged output, categorize the changes into a conventional commit type (e.g., feat, fix, docs, chore,style), determine if a scope is applicable, and then synthesize a concise commit message that follows the format [optional scope]: [optional body] [optional footer(s)]" + }, + "diffy-explain-ai.commitMessageType": { + "type": "string", + "enum": [ + "conventional", + "gitmoji", + "custom" + ], + "enumDescriptions": [ + "Conventional Commits format: [optional scope]: ", + "Gitmoji format with emoji prefixes: 🎨 :: ", + "Custom user-defined format using a template with placeholders" + ], + "default": "conventional", + "markdownDescription": "Choose the commit message format style:\n\n- **conventional**: Standard Conventional Commits format (e.g., `feat: add new feature`)\n- **gitmoji**: Commits with emoji prefixes (e.g., `✨ feat: add new feature`)\n- **custom**: Fully customizable template with placeholders (configure via Custom Commit Prompt setting)" + }, + "diffy-explain-ai.customCommitPrompt": { + "type": "string", + "default": "Generate a commit message for the following git diff.\n\nRequirements:\n- Maximum subject length: {maxLength} characters\n- Use imperative mood\n- Be concise and clear{bodyInstructions}\n\nReturn ONLY the commit message, no explanations.", + "markdownDescription": "Custom commit message generation prompt when **Commit Message Type** is set to `custom`. \n\nSupported placeholders:\n- `{maxLength}` - Maximum subject line length\n- `{bodyInstructions}` - Auto-filled based on Include Commit Body setting\n- `{locale}` - Language/locale for the message\n- `{diff}` - The actual git diff (automatically appended)\n\nExample template:\n```\nGenerate a {locale} commit message following our team convention:\n- Start with JIRA ticket: [PROJ-XXX]\n- Subject max {maxLength} chars\n- Include impact analysis{bodyInstructions}\n```" + }, + "diffy-explain-ai.includeCommitBody": { + "type": "boolean", + "default": false, + "markdownDescription": "Include a detailed body section in commit messages with bullet points explaining the changes. When enabled, commit messages will have:\n\n```\nfeat: add user authentication\n\n- Implement JWT token generation\n- Add login and registration endpoints\n- Create user model and database schema\n```" + }, + "diffy-explain-ai.excludeFilesFromDiff": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + "*.jpg", + "*.png", + "*.gif", + "*.svg", + "*.ico", + "*.woff", + "*.woff2", + "*.ttf", + "*.eot" + ], + "markdownDescription": "File patterns to exclude from AI analysis when generating commit messages. This helps reduce token usage and improve quality by filtering out:\n\n- Lock files (package-lock.json, yarn.lock)\n- Large binary assets (images, fonts)\n- Generated files\n\nSupports glob patterns like `*.log`, `dist/**`, `**/*.min.js`" + }, + "diffy-explain-ai.maxCommitMessageLength": { + "type": "number", + "default": 72, + "minimum": 50, + "maximum": 200, + "markdownDescription": "Maximum character length for commit message subject line. Following best practices:\n\n- **50**: Strict GitHub standard\n- **72**: Common standard (default)\n- **100**: Relaxed limit" } } } diff --git a/src/@types/EventEmitter.d.ts b/src/@types/EventEmitter.d.ts index 321f21e..f5b2415 100644 --- a/src/@types/EventEmitter.d.ts +++ b/src/@types/EventEmitter.d.ts @@ -11,24 +11,15 @@ export default interface TypedEventEmitter { on(name: K, listener: (v: T[K]) => void): this; addListener(event: K, listener: (v: T[K]) => void): this; once(event: K, listener: (v: T[K]) => void): this; - prependListener( - event: K, - listener: (v: T[K]) => void - ): this; - prependOnceListener( - event: K, - listener: (v: T[K]) => void - ): this; - removeListener( - event: K, - listener: (v: T[K]) => void - ): this; + prependListener(event: K, listener: (v: T[K]) => void): this; + prependOnceListener(event: K, listener: (v: T[K]) => void): this; + removeListener(event: K, listener: (v: T[K]) => void): this; off(event: K, listener: (v: T[K]) => void): this; removeAllListeners(event?: K): this; setMaxListeners(n: number): this; getMaxListeners(): number; - listeners(event: K): (v: T[K]) => void[]; - rawListeners(event: K): (v: T[K]) => void[]; + listeners(event: K): (v: T[K]) => undefined[]; + rawListeners(event: K): (v: T[K]) => undefined[]; emit(event: K, args: T[K]): boolean; eventNames(): Array; listenerCount(type: K): number; diff --git a/src/@types/extension.d.ts b/src/@types/extension.d.ts index 774fda5..b595217 100644 --- a/src/@types/extension.d.ts +++ b/src/@types/extension.d.ts @@ -1,6 +1,8 @@ -export type CacheResult = any; +import type OpenAI from "openai"; + +export type CacheResult = string | OpenAI.Chat.Completions.ChatCompletion; export type CacheData = { entity: string; - data?: any; - result: any; + data?: string; + result: CacheResult; }; diff --git a/src/@types/git.d.ts b/src/@types/git.d.ts index 086d8e9..d1896cc 100644 --- a/src/@types/git.d.ts +++ b/src/@types/git.d.ts @@ -5,8 +5,9 @@ // https://github.com/microsoft/vscode/blob/main/extensions/git/src/api/git.d.ts -import { Uri, Event, Disposable, ProviderResult, Command } from "vscode"; -export { ProviderResult } from "vscode"; +import type { Command, Disposable, Event, ProviderResult, Uri } from "vscode"; + +export type { ProviderResult } from "vscode"; export interface Git { readonly path: string; @@ -177,11 +178,9 @@ export interface Repository { getObjectDetails( treeish: string, - path: string + path: string, ): Promise<{ mode: string; object: string; size: number }>; - detectObjectType( - object: string - ): Promise<{ mimetype: string; encoding?: string }>; + detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }>; buffer(ref: string, path: string): Promise; show(ref: string, path: string): Promise; getCommit(ref: string): Promise; @@ -231,7 +230,7 @@ export interface Repository { remoteName?: string, branchName?: string, setUpstream?: boolean, - force?: ForcePushMode + force?: ForcePushMode, ): Promise; blame(path: string): Promise; @@ -279,7 +278,7 @@ export interface PushErrorHandler { repository: Repository, remote: Remote, refspec: string, - error: Error & { gitErrorCode: GitErrorCodes } + error: Error & { gitErrorCode: GitErrorCodes }, ): Promise; } @@ -307,9 +306,7 @@ export interface API { registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable; registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; registerCredentialsProvider(provider: CredentialsProvider): Disposable; - registerPostCommitCommandsProvider( - provider: PostCommitCommandsProvider - ): Disposable; + registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; } diff --git a/src/Diffy.ts b/src/Diffy.ts index 45adc56..8766ddf 100644 --- a/src/Diffy.ts +++ b/src/Diffy.ts @@ -1,9 +1,10 @@ import * as vscode from "vscode"; -import { env, ExtensionContext } from "vscode"; +import { type ExtensionContext, env } from "vscode"; import { EventType } from "./@types/EventType"; import BaseDiffy from "./BaseDiffy"; import GitService from "./service/GitService"; import OpenAiService from "./service/OpenAiService"; +import VsCodeLlmService from "./service/VsCodeLlmService"; import WindowService from "./service/WindowService"; import WorkspaceService from "./service/WorkspaceService"; import { sendToOutput } from "./utils/log"; @@ -12,17 +13,18 @@ class Diffy extends BaseDiffy { static _instance: Diffy; private gitService: GitService | null = null; private _openAIService: OpenAiService | null = null; + private _vsCodeLlmService: VsCodeLlmService | null = null; private workspaceService: WorkspaceService | null = null; isEnabled = false; - private _windowsService: any; + private _windowsService: WindowService | null = null; context!: ExtensionContext; constructor(context: ExtensionContext) { - if (Diffy._instance) { - return Diffy._instance; - } super(); - this.context = context; + if (!Diffy._instance) { + Diffy._instance = this; + this.context = context; + } } /** @@ -57,6 +59,30 @@ class Diffy extends BaseDiffy { return this._openAIService; } + /** + * If the _vsCodeLlmService property is not defined, then create a new instance of the VsCodeLlmService + * class and assign it to the _vsCodeLlmService property. + * @returns The VsCodeLlmService object. + */ + getVsCodeLlmService(): VsCodeLlmService { + if (!this._vsCodeLlmService) { + this._vsCodeLlmService = VsCodeLlmService.getInstance(); + } + return this._vsCodeLlmService; + } + + /** + * Gets the appropriate AI service based on user settings + * @returns The selected AI service (OpenAI or VS Code LLM) + */ + getAIService(): AIService { + const provider = this.workspaceService?.getAiServiceProvider(); + if (provider === "vscode-lm") { + return this.getVsCodeLlmService(); + } + return this.getOpenAPIService(); + } + getWindowService(): WindowService { if (!this._windowsService) { this._windowsService = WindowService.getInstance(); @@ -71,11 +97,17 @@ class Diffy extends BaseDiffy { if (!this.gitService?.checkAndWarnRepoExist()) { return; } - /* Checking if the api key is defined. */ - const apiKey = this.workspaceService?.getOpenAIKey(); - if (!apiKey) { - return; + + const provider = this.workspaceService?.getAiServiceProvider(); + + // Check if API key is required (for OpenAI) + if (provider === "openai") { + const apiKey = this.workspaceService?.getOpenAIKey(); + if (!apiKey) { + return; + } } + /* Getting the current repo. */ const repo = this.gitService?.getCurrentRepo(); if (!repo) { @@ -94,19 +126,53 @@ class Diffy extends BaseDiffy { if (!diff) { return; } - /* OpenAPI */ - const changes = await this.getOpenAPIService().getExplainedChanges( - diff, - apiKey, - nameOnly - ); + + /* Get AI Service based on provider */ + const aiService = this.getAIService(); + let changes: string | null = null; + + if (provider === "openai") { + const apiKey = this.workspaceService?.getOpenAIKey(); + if (!apiKey) { + return; + } + changes = await (aiService as OpenAiService).getExplainedChanges(diff, apiKey, nameOnly); + } else { + // VS Code LLM - try with fallback to OpenAI if it fails + try { + changes = await aiService.getExplainedChanges("", diff); + } catch (error) { + // If VS Code LM fails with model not supported error, try OpenAI as fallback + if ( + error instanceof Error && + (error.message.includes("model_not_supported") || + error.message.includes("Model is not supported")) + ) { + vscode.window.showInformationMessage( + "VS Code Language Model not supported for this request. Falling back to OpenAI...", + ); + const apiKey = this.workspaceService?.getOpenAIKey(); + if (apiKey) { + changes = await this.getOpenAPIService().getExplainedChanges(diff, apiKey, nameOnly); + } else { + vscode.window.showErrorMessage( + "VS Code Language Model failed and no OpenAI API key configured. Please configure OpenAI API key in settings.", + ); + return; + } + } else { + throw error; // Re-throw other errors + } + } + } + if (changes) { this.getWindowService().showExplainedResultWebviewPane(changes); } } /** - * It takes the code that you've changed in your current git branch, sends it to OpenAI's API, and + * It takes the code that you've changed in your current git branch, sends it to AI service, and * then copies the response to your clipboard * @returns The return value is a string. */ @@ -117,11 +183,17 @@ class Diffy extends BaseDiffy { if (!this.gitService?.checkAndWarnRepoExist()) { return; } - /* Checking if the api key is defined. */ - const apiKey = this.workspaceService?.getOpenAIKey(); - if (!apiKey) { - return; + + const provider = this.workspaceService?.getAiServiceProvider(); + + // Check if API key is required (for OpenAI) + if (provider === "openai") { + const apiKey = this.workspaceService?.getOpenAIKey(); + if (!apiKey) { + return; + } } + /* Getting the current repo. */ const repo = this.gitService?.getCurrentRepo(); if (!repo) { @@ -140,12 +212,46 @@ class Diffy extends BaseDiffy { if (!diff) { return; } - /* OpenAPI */ - const changes = await this.getOpenAPIService().getExplainedChanges( - diff, - apiKey, - nameOnly - ); + + /* Get AI Service based on provider */ + const aiService = this.getAIService(); + let changes: string | null = null; + + if (provider === "openai") { + const apiKey = this.workspaceService?.getOpenAIKey(); + if (!apiKey) { + return; + } + changes = await (aiService as OpenAiService).getExplainedChanges(diff, apiKey, nameOnly); + } else { + // VS Code LLM - try with fallback to OpenAI if it fails + try { + changes = await aiService.getExplainedChanges("", diff); + } catch (error) { + // If VS Code LM fails with model not supported error, try OpenAI as fallback + if ( + error instanceof Error && + (error.message.includes("model_not_supported") || + error.message.includes("Model is not supported")) + ) { + vscode.window.showInformationMessage( + "VS Code Language Model not supported for this request. Falling back to OpenAI...", + ); + const apiKey = this.workspaceService?.getOpenAIKey(); + if (apiKey) { + changes = await this.getOpenAPIService().getExplainedChanges(diff, apiKey, nameOnly); + } else { + vscode.window.showErrorMessage( + "VS Code Language Model failed and no OpenAI API key configured. Please configure OpenAI API key in settings.", + ); + return; + } + } else { + throw error; // Re-throw other errors + } + } + } + /* Copying the changes to the clipboard and showing the changes in the message box. */ if (changes) { env.clipboard.writeText(changes); @@ -154,8 +260,8 @@ class Diffy extends BaseDiffy { } /** - * It takes the current git diff, sends it to OpenAI, and then copies the response to the clipboard. - * @returns The commit message. + * It takes the diff of the current branch, sends it to the AI service, and then copies the response to clipboard. + * @returns a promise. */ async generateCommitMessageToClipboard() { if (!this.workspaceService?.checkAndWarnWorkSpaceExist()) { @@ -164,10 +270,15 @@ class Diffy extends BaseDiffy { if (!this.gitService?.checkAndWarnRepoExist()) { return; } - /* Checking if the api key is defined. */ - const apiKey = this.workspaceService?.getOpenAIKey(); - if (!apiKey) { - return; + + const provider = this.workspaceService?.getAiServiceProvider(); + + // Check if API key is required (for OpenAI) + if (provider === "openai") { + const apiKey = this.workspaceService?.getOpenAIKey(); + if (!apiKey) { + return; + } } /* Getting the current repo. */ @@ -188,12 +299,49 @@ class Diffy extends BaseDiffy { if (!diff) { return; } - /* OpenAPI */ - const changes = await this.getOpenAPIService().getCommitMessageFromDiff( - diff, - apiKey, - nameOnly - ); + + /* Get AI Service based on provider */ + let changes: string | null = null; + + if (provider === "openai") { + const apiKey = this.workspaceService?.getOpenAIKey(); + if (!apiKey) { + return; + } + changes = await this.getOpenAPIService().getCommitMessageFromDiff(diff, apiKey, nameOnly); + } else { + // VS Code LLM - try with fallback to OpenAI if it fails + try { + changes = await this.getVsCodeLlmService().getCommitMessageFromDiff(diff, nameOnly); + } catch (error) { + // If VS Code LM fails with model not supported error, try OpenAI as fallback + if ( + error instanceof Error && + (error.message.includes("model_not_supported") || + error.message.includes("Model is not supported")) + ) { + vscode.window.showInformationMessage( + "VS Code Language Model not supported for this request. Falling back to OpenAI...", + ); + const apiKey = this.workspaceService?.getOpenAIKey(); + if (apiKey) { + changes = await this.getOpenAPIService().getCommitMessageFromDiff( + diff, + apiKey, + nameOnly, + ); + } else { + vscode.window.showErrorMessage( + "VS Code Language Model failed and no OpenAI API key configured. Please configure OpenAI API key in settings.", + ); + return; + } + } else { + throw error; // Re-throw other errors + } + } + } + if (changes) { env.clipboard.writeText(changes); this.showInformationMessage(changes); @@ -201,7 +349,7 @@ class Diffy extends BaseDiffy { } /** - * It takes the diff of the current branch, sends it to the OpenAI API, and then sets the commit + * It takes the diff of the current branch, sends it to the AI service, and then sets the commit * message to the input box * @returns a promise. */ @@ -209,7 +357,7 @@ class Diffy extends BaseDiffy { progress?: vscode.Progress<{ message?: string | undefined; increment?: number | undefined; - }> + }>, ) { if (!this.workspaceService?.checkAndWarnWorkSpaceExist()) { return; @@ -217,10 +365,15 @@ class Diffy extends BaseDiffy { if (!this.gitService?.checkAndWarnRepoExist()) { return; } - /* Checking if the api key is defined. */ - const apiKey = this.workspaceService?.getOpenAIKey(); - if (!apiKey) { - return; + + const provider = this.workspaceService?.getAiServiceProvider(); + + // Check if API key is required (for OpenAI) + if (provider === "openai") { + const apiKey = this.workspaceService?.getOpenAIKey(); + if (!apiKey) { + return; + } } /* Getting the current repo. */ @@ -241,13 +394,59 @@ class Diffy extends BaseDiffy { if (!diff) { return; } - /* OpenAPI */ - const changes = await this.getOpenAPIService().getCommitMessageFromDiff( - diff, - apiKey, - nameOnly, - progress - ); + + /* Get AI Service based on provider */ + let changes: string | null = null; + + if (provider === "openai") { + const apiKey = this.workspaceService?.getOpenAIKey(); + if (!apiKey) { + return; + } + changes = await this.getOpenAPIService().getCommitMessageFromDiff( + diff, + apiKey, + nameOnly, + progress, + ); + } else { + // VS Code LLM - try with fallback to OpenAI if it fails + try { + changes = await this.getVsCodeLlmService().getCommitMessageFromDiff( + diff, + nameOnly, + progress, + ); + } catch (error) { + // If VS Code LM fails with model not supported error, try OpenAI as fallback + if ( + error instanceof Error && + (error.message.includes("model_not_supported") || + error.message.includes("Model is not supported")) + ) { + vscode.window.showInformationMessage( + "VS Code Language Model not supported for this request. Falling back to OpenAI...", + ); + const apiKey = this.workspaceService?.getOpenAIKey(); + if (apiKey) { + changes = await this.getOpenAPIService().getCommitMessageFromDiff( + diff, + apiKey, + nameOnly, + progress, + ); + } else { + vscode.window.showErrorMessage( + "VS Code Language Model failed and no OpenAI API key configured. Please configure OpenAI API key in settings.", + ); + return; + } + } else { + throw error; // Re-throw other errors + } + } + } + if (changes) { /* Setting the commit message to the input box. */ this.gitService?.setCommitMessageToInputBox(repo, changes); @@ -261,6 +460,7 @@ class Diffy extends BaseDiffy { this.isEnabled = false; this.gitService = null; this._openAIService = null; + this._vsCodeLlmService = null; this.workspaceService = null; } } diff --git a/src/extension.ts b/src/extension.ts index d668b9a..0c68b45 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,55 +10,43 @@ export function activate(context: vscode.ExtensionContext) { app.init(); context.subscriptions.push( - vscode.commands.registerCommand( - "diffy-explain-ai.explainDiffClipboard", - () => { - app?.explainDiffToClipboard(); - } - ) + vscode.commands.registerCommand("diffy-explain-ai.explainDiffClipboard", () => { + return app?.explainDiffToClipboard(); + }), ); context.subscriptions.push( - vscode.commands.registerCommand( - "diffy-explain-ai.generateCommitMessage", - async () => { - vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - cancellable: false, - title: "Generating...\n", - }, - async (progress) => { - progress.report({ increment: 0 }); - - await app?.generateCommitMessageToSCM(progress); - - progress.report({ - message: "Commit message generated.", - increment: 100, - }); - } - ); - } - ) + vscode.commands.registerCommand("diffy-explain-ai.generateCommitMessage", () => { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: "Generating...\n", + }, + async (progress) => { + progress.report({ increment: 0 }); + + await app?.generateCommitMessageToSCM(progress); + + progress.report({ + message: "Commit message generated.", + increment: 100, + }); + }, + ); + }), ); context.subscriptions.push( - vscode.commands.registerCommand( - "diffy-explain-ai.generateCommitMessageClipboard", - () => { - app?.generateCommitMessageToClipboard(); - } - ) + vscode.commands.registerCommand("diffy-explain-ai.generateCommitMessageClipboard", () => { + return app?.generateCommitMessageToClipboard(); + }), ); context.subscriptions.push( - vscode.commands.registerCommand( - "diffy-explain-ai.explainAndPreview", - () => { - app?.explainAndPreview(); - } - ) + vscode.commands.registerCommand("diffy-explain-ai.explainAndPreview", () => { + return app?.explainAndPreview(); + }), ); } diff --git a/src/service/CacheService.ts b/src/service/CacheService.ts index 0eb20d5..3b5aaab 100644 --- a/src/service/CacheService.ts +++ b/src/service/CacheService.ts @@ -1,4 +1,4 @@ -import { CacheData, CacheResult } from "../@types/extension"; +import type { CacheData, CacheResult } from "../@types/extension"; export class CacheService { private static _instance: CacheService; @@ -17,7 +17,7 @@ export class CacheService { /* A method that takes in a entity and data. It then checks if the record exists and if it doesn't it pushes it to the cache. */ - public set = (entity: string, data: any, result: any): void => { + public set = (entity: string, data: string, result: CacheResult): void => { if (!this.recordExists(entity, data)) { this.cache.push({ entity: entity, @@ -29,7 +29,7 @@ export class CacheService { /* A method that takes in entity and data. It then checks if the record exists and if it doesn't it pushes it to the cache. */ - public get = (entity: string, data: any): CacheResult | null => { + public get = (entity: string, data: string): CacheResult | null => { const cacheRecord = this.cache.find((x) => { if (data) { return x.entity === entity && x.data === data; @@ -47,7 +47,7 @@ export class CacheService { /* It's a method that takes in entity and data . It then checks if the record exists and if it doesn't it pushes it to the cache. */ - public recordExists = (entity: string, data: any): boolean => { + public recordExists = (entity: string, data: string): boolean => { return !!this.get(entity, data); }; } diff --git a/src/service/GitService.ts b/src/service/GitService.ts index b097258..87057f8 100644 --- a/src/service/GitService.ts +++ b/src/service/GitService.ts @@ -1,8 +1,8 @@ -import { window, Extension, extensions } from "vscode"; - -import { API as GitApi, GitExtension, Repository } from "../@types/git"; -import { CONSTANTS } from "../Constants"; import simpleGit from "simple-git"; +import { type Extension, extensions, window } from "vscode"; +import type { API as GitApi, GitExtension, Repository } from "../@types/git"; +import { CONSTANTS } from "../Constants"; +import WorkspaceService from "./WorkspaceService"; class GitService { static _instance: GitService; @@ -46,9 +46,7 @@ class GitService { if (this.vscodeGitApi === null) { const api = await this.getVscodeGitApi(); if (api === undefined) { - this.showErrorMessage( - "Please make sure git repo initiated or scm plugin working" - ); + this.showErrorMessage("Please make sure git repo initiated or scm plugin working"); this.isEnabled = false; return; } @@ -98,20 +96,16 @@ class GitService { window.showInformationMessage(`${CONSTANTS.extensionShortName}: ${msg}`); } - async getDiffAndWarnUser( - repo: Repository, - cached = true, - nameOnly?: boolean - ) { + async getDiffAndWarnUser(repo: Repository, cached = true, nameOnly?: boolean) { const diff = await this.getGitDiff(repo, cached, nameOnly); if (!diff) { if (cached) { const diffUncached = await repo.diff(false); - diffUncached - ? this.showInformationMessage( - "warning: please stage your git changes" - ) - : this.showInformationMessage("No Changes"); + if (diffUncached) { + this.showInformationMessage("warning: please stage your git changes"); + } else { + this.showInformationMessage("No Changes"); + } return null; } this.showInformationMessage("No changes"); @@ -119,20 +113,90 @@ class GitService { return diff; } + /** + * Check if a file path matches any of the exclusion patterns + * @param filePath - The file path to check + * @param patterns - Array of glob patterns to match against + * @returns true if the file should be excluded + */ + private shouldExcludeFile(filePath: string, patterns: string[]): boolean { + for (const pattern of patterns) { + // Simple glob pattern matching + if (pattern.includes("*")) { + const regex = new RegExp( + `^${pattern.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".")}$`, + ); + if (regex.test(filePath)) { + return true; + } + } else if (filePath.includes(pattern)) { + return true; + } + } + return false; + } + + /** + * Filter diff output to exclude specified file patterns + * @param diff - The raw git diff output + * @param excludePatterns - Patterns to exclude + * @returns Filtered diff output + */ + private filterDiffByExclusions(diff: string, excludePatterns: string[]): string { + if (!excludePatterns || excludePatterns.length === 0) { + return diff; + } + + // Split diff into file sections (each starts with "diff --git") + const fileSections = diff.split(/(?=diff --git)/); + const filteredSections: string[] = []; + + for (const section of fileSections) { + if (!section.trim()) { + continue; + } + + // Extract file path from the diff header + // Format: "diff --git a/path/to/file b/path/to/file" + const fileMatch = section.match(/diff --git a\/(.+?) b\//); + if (!fileMatch) { + // Keep sections we can't parse + filteredSections.push(section); + continue; + } + + const filePath = fileMatch[1]; + if (!this.shouldExcludeFile(filePath, excludePatterns)) { + filteredSections.push(section); + } + } + + return filteredSections.join(""); + } + /** * Get the diff in the git repository. * @returns The diff object is being returned. */ - async getGitDiff(repo: Repository, cachedInput = true, nameOnly?: boolean) { + async getGitDiff(repo: Repository, _cachedInput = true, nameOnly?: boolean) { // let diff = await repo.diff(cached); const git = simpleGit(repo.rootUri.fsPath); let diff: string | null = ""; + + // Get exclusion patterns from settings + const excludePatterns = WorkspaceService.getInstance().getExcludeFilesFromDiff(); + if (!nameOnly) { diff = await git.diff(["--cached"]).catch((error) => { this.showErrorMessage("git repository not found"); console.error(error); return null; }); + + // Apply file filtering if diff was successful + if (diff && excludePatterns.length > 0) { + diff = this.filterDiffByExclusions(diff, excludePatterns); + } } else { diff = await git.diff(["--cached", "--name-status"]).catch((error) => { this.showErrorMessage("git repository not found"); @@ -150,9 +214,7 @@ class GitService { */ setCommitMessageToInputBox(repo: Repository, message: string) { const previousValue = repo.inputBox.value; - repo.inputBox.value = previousValue - ? previousValue + " \n" + message - : message; + repo.inputBox.value = previousValue ? `${previousValue} \n${message}` : message; } /** @@ -161,13 +223,9 @@ class GitService { */ private async getVscodeGitApi(): Promise { try { - const extension = extensions.getExtension( - "vscode.git" - ) as Extension; + const extension = extensions.getExtension("vscode.git") as Extension; if (extension !== undefined) { - const gitExtension = extension.isActive - ? extension.exports - : await extension.activate(); + const gitExtension = extension.isActive ? extension.exports : await extension.activate(); return gitExtension.getAPI(1); } diff --git a/src/service/OpenAiService.ts b/src/service/OpenAiService.ts index 6e33fa1..a410ece 100644 --- a/src/service/OpenAiService.ts +++ b/src/service/OpenAiService.ts @@ -1,5 +1,5 @@ import OpenAI from "openai"; -import * as vscode from "vscode"; +import type * as vscode from "vscode"; import { window } from "vscode"; import { clearOutput, sendToOutput } from "../utils/log"; import { CacheService } from "./CacheService"; @@ -17,12 +17,9 @@ export interface Error { class OpenAiService implements AIService { static _instance: OpenAiService; - cacheService!: CacheService; + cacheService: CacheService; - constructor() { - if (OpenAiService._instance) { - return OpenAiService._instance; - } + private constructor() { this.cacheService = CacheService.getInstance(); } @@ -46,35 +43,22 @@ class OpenAiService implements AIService { async getCommitMessageFromDiff( code: string, openAIKey: string, - nameOnly?: boolean, + _nameOnly?: boolean, progress?: vscode.Progress<{ message?: string | undefined; increment?: number | undefined; - }> + }>, ): Promise { - let gitCmd = "git diff --cached"; - if (nameOnly) { - gitCmd = "git diff --cached --name-status"; - } const instructions = WorkspaceService.getInstance().getAIInstructions(); if (!instructions) { return null; } - let response = await this.getFromOpenApi( - instructions, - code, - openAIKey, - progress - ); - if ( - response && - response.choices.length > 0 && - response.choices[0].message - ) { + const response = await this.getFromOpenApi(instructions, code, openAIKey, progress); + if (response && response.choices.length > 0 && response.choices[0].message) { let message = String(response?.choices[0].message.content); message = message.trim(); - message = message.replace(/^\"/gm, ""); - message = message.replace(/\"$/gm, ""); + message = message.replace(/^"/gm, ""); + message = message.replace(/"$/gm, ""); return message; } return null; @@ -86,11 +70,7 @@ class OpenAiService implements AIService { * @param {string} code - the git diff you want to get the explanation for * @returns The explanation of the git diff. */ - async getExplainedChanges( - code: string, - openAIKey?: string, - nameOnly?: boolean - ) { + async getExplainedChanges(code: string, openAIKey?: string, nameOnly?: boolean) { let gitCmd = "git diff --cached"; if (nameOnly) { gitCmd = "git diff --cached --name-status"; @@ -99,16 +79,12 @@ class OpenAiService implements AIService { "You are a bot explains the changes from the result of '" + gitCmd + "' that user given. commit message should be a multiple lines where first line doesn't exceeds '50' characters by following commit message guidelines based on the given git diff changes without mentioning itself"; - let response = await this.getFromOpenApi(instructions, code, openAIKey); - if ( - response && - response.choices.length > 0 && - response.choices[0].message - ) { + const response = await this.getFromOpenApi(instructions, code, openAIKey); + if (response && response.choices.length > 0 && response.choices[0].message) { let message = String(response?.choices[0].message.content); message = message.trim(); - message = message.replace(/^\"/gm, ""); - message = message.replace(/\"$/gm, ""); + message = message.replace(/^"/gm, ""); + message = message.replace(/"$/gm, ""); return message; } return null; @@ -127,7 +103,7 @@ class OpenAiService implements AIService { progress?: vscode.Progress<{ message?: string | undefined; increment?: number | undefined; - }> + }>, ) { const openAiClient = new OpenAI({ apiKey: openAIKey }); const model = WorkspaceService.getInstance().getGptModel(); @@ -135,7 +111,7 @@ class OpenAiService implements AIService { if (exist) { const result = this.cacheService.get( model, - instructions + prompt + instructions + prompt, ) as OpenAI.Chat.Completions.ChatCompletion; sendToOutput(`result: ${JSON.stringify(result)}`); return result; @@ -174,58 +150,74 @@ class OpenAiService implements AIService { sendToOutput(`model: ${params.model}`); sendToOutput(`max_tokens: ${params.max_tokens}`); sendToOutput(`temperature: ${params.temperature}`); - const response = await openAiClient.chat.completions - .create(params) - .then((value) => { - sendToOutput(`result success: ${JSON.stringify(value)}`); - progress?.report({ increment: 49 }); - return value; - }) - .catch(async (reason) => { - console.error(reason.response); - sendToOutput(`result failed: ${JSON.stringify(reason)}`); - if (typeof reason === "string" || reason instanceof String) { - window.showErrorMessage(`OpenAI Error: ${reason} `); - return undefined; - } + + let response: OpenAI.Chat.Completions.ChatCompletion | undefined; + try { + response = await openAiClient.chat.completions.create(params); + sendToOutput(`result success: ${JSON.stringify(response)}`); + progress?.report({ increment: 49 }); + } catch (reason: unknown) { + console.error(reason); + sendToOutput(`result failed: ${JSON.stringify(reason)}`); + + // Type guard for error with response property + const hasResponse = ( + err: unknown, + ): err is { + response?: { + statusText?: string; + status?: number; + data?: { error?: { message?: string; type?: string } }; + }; + } => { + return typeof err === "object" && err !== null && "response" in err; + }; + + if (typeof reason === "string" || reason instanceof String) { + window.showErrorMessage(`OpenAI Error: ${reason} `); + return undefined; + } + + if (hasResponse(reason)) { if (reason.response?.statusText) { window.showErrorMessage( - `OpenAI Error: ${ - reason.response?.data.error?.message || reason.response.statusText - } ` + `OpenAI Error: ${reason.response?.data?.error?.message || reason.response.statusText} `, ); } else { window.showErrorMessage("OpenAI Error"); } - if (reason?.response?.status && openAIKey) { - if (reason?.response?.status === 429) { + + if (reason.response?.status && openAIKey) { + if (reason.response.status === 429) { window.showInformationMessage( - "Caution: In case the API key has expired, please remove it from the extension settings in order to continue using the default proxy server." + "Caution: In case the API key has expired, please remove it from the extension settings in order to continue using the default proxy server.", ); // return await this.getFromOpenApi(prompt, undefined, progress); } } - if (reason.response?.data.error?.type === "invalid_request_error") { + + if (reason.response?.data?.error?.type === "invalid_request_error") { window.showErrorMessage( - "Diffy Error: There was an issue. Server is experiencing downtime/busy. Please try again later." + "Diffy Error: There was an issue. Server is experiencing downtime/busy. Please try again later.", ); progress?.report({ increment: 1, message: "\nFailed.", }); - } else if (reason.response?.data.error?.message) { - window.showErrorMessage( - `Diffy Error: ${reason.response?.data.error?.message}` - ); + } else if (reason.response?.data?.error?.message) { + window.showErrorMessage(`Diffy Error: ${reason.response.data.error.message}`); progress?.report({ increment: 1, message: "\nFailed.", }); } - return undefined; - }); + } else { + window.showErrorMessage("OpenAI Error"); + } + + return undefined; + } if ( - response && response?.choices[0].message.content && response?.choices[0].message.content !== "" && response?.choices[0].message.content !== "\n" diff --git a/src/service/VsCodeLlmService.ts b/src/service/VsCodeLlmService.ts index 8733988..5da991f 100644 --- a/src/service/VsCodeLlmService.ts +++ b/src/service/VsCodeLlmService.ts @@ -6,12 +6,9 @@ import WorkspaceService from "./WorkspaceService"; class VsCodeLlmService implements AIService { static _instance: VsCodeLlmService; - cacheService!: CacheService; + cacheService: CacheService; - constructor() { - if (VsCodeLlmService._instance) { - return VsCodeLlmService._instance; - } + private constructor() { this.cacheService = CacheService.getInstance(); } @@ -35,30 +32,126 @@ class VsCodeLlmService implements AIService { */ async getCommitMessageFromDiff( code: string, - nameOnly?: boolean, + _nameOnly?: boolean, progress?: vscode.Progress<{ message?: string | undefined; increment?: number | undefined; - }> + }>, ): Promise { - const instructions = WorkspaceService.getInstance().getAIInstructions(); - if (!instructions) { - return null; + const workspaceService = WorkspaceService.getInstance(); + const commitType = workspaceService.getCommitMessageType(); + const includeBody = workspaceService.getIncludeCommitBody(); + const maxLength = workspaceService.getMaxCommitMessageLength(); + const customInstructions = workspaceService.getAIInstructions(); + + // Build enhanced prompt based on commit type and settings + let instructions = ""; + + if (commitType === "custom") { + // Custom user-defined template with placeholder replacement + const customTemplate = workspaceService.getCustomCommitPrompt(); + + // Prepare body instructions based on includeBody setting + const bodyInstructions = includeBody + ? "\n- Include a body section with 2-4 bullet points explaining the changes" + : ""; + + // Replace placeholders + instructions = customTemplate + .replace(/{maxLength}/g, String(maxLength)) + .replace(/{bodyInstructions}/g, bodyInstructions) + .replace(/{locale}/g, "en") // Could be made configurable if needed + .replace(/{diff}/g, ""); // We append diff separately + + // Add custom instructions if provided (unless already in template) + if (customInstructions && !customTemplate.includes(customInstructions)) { + instructions += `\n\nADDITIONAL INSTRUCTIONS:\n${customInstructions}`; + } + } else if (commitType === "gitmoji") { + instructions = `You are an expert Git commit message generator that uses Gitmoji (emoji-based commits). + +Analyze the provided git diff and generate a commit message following the Gitmoji specification: + +FORMAT: [optional scope]: + +Common Gitmoji mappings: +- ✨ :sparkles: - New feature (feat) +- 🐛 :bug: - Bug fix (fix) +- 📝 :memo: - Documentation (docs) +- 💄 :lipstick: - UI/styling (style) +- ♻️ :recycle: - Code refactoring (refactor) +- ⚡️ :zap: - Performance improvement (perf) +- ✅ :white_check_mark: - Tests (test) +- 🔧 :wrench: - Configuration (chore) +- 🔨 :hammer: - Build/tooling (build) +- 🚀 :rocket: - Deployment (ci) + +REQUIREMENTS: +1. Subject line must NOT exceed ${maxLength} characters +2. Use imperative mood (e.g., "add" not "added") +3. Do not end subject with a period +4. Start with the appropriate emoji${ + includeBody + ? "\n5. Include a body section with 2-4 bullet points explaining key changes" + : "" + } + +${customInstructions ? `\nADDITIONAL INSTRUCTIONS:\n${customInstructions}\n` : ""} + +Return ONLY the commit message, no explanations or surrounding text.`; + } else { + // Conventional Commits format + instructions = `You are an expert Git commit message generator following Conventional Commits specification. + +Analyze the provided git diff and generate a commit message following this format: + +[optional scope]: +${includeBody ? "\n[optional body]\n" : ""} +[optional footer(s)] + +COMMIT TYPES: +- feat: A new feature +- fix: A bug fix +- docs: Documentation only changes +- style: Changes that don't affect code meaning (formatting, missing semicolons, etc.) +- refactor: Code change that neither fixes a bug nor adds a feature +- perf: Code change that improves performance +- test: Adding missing tests or correcting existing tests +- build: Changes to build system or external dependencies +- ci: Changes to CI configuration files and scripts +- chore: Other changes that don't modify src or test files +- revert: Reverts a previous commit + +REQUIREMENTS: +1. Subject line must NOT exceed ${maxLength} characters +2. Use imperative mood (e.g., "add" not "added") +3. Do not capitalize first letter after type +4. Do not end subject with a period +5. Choose the most specific scope when applicable (e.g., "auth", "api", "ui")${ + includeBody + ? "\n6. Include a body section with 2-4 bullet points explaining:\n - What changed\n - Why it changed\n - Any important implementation details" + : "" + } + +${customInstructions ? `\nADDITIONAL INSTRUCTIONS:\n${customInstructions}\n` : ""} + +Return ONLY the commit message, no explanations or surrounding text.`; } - - const response = await this.getFromVsCodeLlm( - instructions, - code, - progress - ); - - if (response) { - let message = response.trim(); - message = message.replace(/^"/gm, ""); - message = message.replace(/"$/gm, ""); - return message; + + try { + const response = await this.getFromVsCodeLlm(instructions, code, progress); + + if (response) { + let message = response.trim(); + message = message.replace(/^"/gm, ""); + message = message.replace(/"$/gm, ""); + return message; + } + return null; + } catch (error) { + // Re-throw the error so it can be caught by the calling function + throw error; } - return null; } /** @@ -67,23 +160,25 @@ class VsCodeLlmService implements AIService { * @param {string} string2 - the second parameter (diff code) * @returns The explanation of the git diff. */ - async getExplainedChanges( - string1: string, - string2: string - ): Promise { + async getExplainedChanges(_string1: string, string2: string): Promise { const instructions = "You are a bot that explains the changes from the result of 'git diff --cached' that user given. commit message should be a multiple lines where first line doesn't exceed '50' characters by following commit message guidelines based on the given git diff changes without mentioning itself"; - + // Use string2 as the code/diff, string1 is typically instructions but we use our own - const response = await this.getFromVsCodeLlm(instructions, string2); - - if (response) { - let message = response.trim(); - message = message.replace(/^"/gm, ""); - message = message.replace(/"$/gm, ""); - return message; + try { + const response = await this.getFromVsCodeLlm(instructions, string2); + + if (response) { + let message = response.trim(); + message = message.replace(/^"/gm, ""); + message = message.replace(/"$/gm, ""); + return message; + } + return null; + } catch (error) { + // Re-throw the error so it can be caught by the calling function + throw error; } - return null; } /** @@ -99,10 +194,10 @@ class VsCodeLlmService implements AIService { progress?: vscode.Progress<{ message?: string | undefined; increment?: number | undefined; - }> + }>, ): Promise { const vscodeLmModel = WorkspaceService.getInstance().getVsCodeLmModel(); - + // Check cache const cacheKey = instructions + prompt; const exist = this.cacheService.recordExists(vscodeLmModel, cacheKey); @@ -120,27 +215,149 @@ class VsCodeLlmService implements AIService { // Select the appropriate model based on settings let models: vscode.LanguageModelChat[] = []; - - if (vscodeLmModel === "copilot-gpt-4o") { + + if (vscodeLmModel === "auto") { + // Try to select the best available model in order of preference + const preferredFamilies = [ + "gpt-4o", + "o1", + "gpt-4", + "gpt-4-turbo", + "o1-mini", + "gpt-3.5-turbo", + "o1-preview", + "gpt-3.5", + ]; + for (const family of preferredFamilies) { + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: family, + }); + if (models.length > 0) { + break; + } + } + // If no specific family models found, try any copilot model + if (models.length === 0) { + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + }); + } + } else if (vscodeLmModel === "copilot-gpt-4o") { models = await vscode.lm.selectChatModels({ vendor: "copilot", - family: "gpt-4o" + family: "gpt-4o", }); + if (models.length === 0) { + // Fallback to any GPT-4 model + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "gpt-4", + }); + } + } else if (vscodeLmModel === "copilot-gpt-4") { + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "gpt-4", + }); + if (models.length === 0) { + // Fallback to any available copilot model + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + }); + } + } else if (vscodeLmModel === "copilot-gpt-4-turbo") { + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "gpt-4-turbo", + }); + if (models.length === 0) { + // Fallback to any GPT-4 model + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "gpt-4", + }); + } } else if (vscodeLmModel === "copilot-gpt-3.5-turbo") { models = await vscode.lm.selectChatModels({ vendor: "copilot", - family: "gpt-3.5-turbo" + family: "gpt-3.5-turbo", }); + if (models.length === 0) { + // Fallback to any GPT-3.5 model + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "gpt-3.5", + }); + } + } else if (vscodeLmModel === "copilot-gpt-3.5") { + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "gpt-3.5", + }); + if (models.length === 0) { + // Fallback to any available copilot model + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + }); + } + } else if (vscodeLmModel === "copilot-o1") { + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "o1", + }); + if (models.length === 0) { + // Fallback to any available copilot model + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + }); + } + } else if (vscodeLmModel === "copilot-o1-mini") { + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "o1-mini", + }); + if (models.length === 0) { + // Fallback to o1 + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "o1", + }); + } + } else if (vscodeLmModel === "copilot-o1-preview") { + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "o1-preview", + }); + if (models.length === 0) { + // Fallback to o1 + models = await vscode.lm.selectChatModels({ + vendor: "copilot", + family: "o1", + }); + } } else { // Default: try to select any copilot model models = await vscode.lm.selectChatModels({ - vendor: "copilot" + vendor: "copilot", }); + + // If still no models available, show a more specific error + if (models.length === 0) { + window.showErrorMessage( + "No GitHub Copilot models available. Please ensure GitHub Copilot is installed, enabled, and you have an active subscription.", + ); + progress?.report({ + increment: 1, + message: "\nFailed - No Copilot models available.", + }); + return undefined; + } } if (models.length === 0) { window.showErrorMessage( - "No language models available. Please ensure GitHub Copilot is installed and you are signed in." + "No language models available. Please ensure GitHub Copilot is installed and you are signed in.", ); progress?.report({ increment: 1, @@ -154,17 +371,14 @@ class VsCodeLlmService implements AIService { progress?.report({ increment: 30 }); - // Prepare messages - const messages = [ - vscode.LanguageModelChatMessage.User(instructions), - vscode.LanguageModelChatMessage.User(prompt), - ]; + // Prepare messages - try a simpler format that should be more compatible + const messages = [vscode.LanguageModelChatMessage.User(`${instructions}\n\n${prompt}`)]; - // Send request + // Send request with minimal options const chatResponse = await model.sendRequest( messages, {}, - new vscode.CancellationTokenSource().token + new vscode.CancellationTokenSource().token, ); progress?.report({ increment: 40 }); @@ -190,20 +404,21 @@ class VsCodeLlmService implements AIService { await new Promise((f) => setTimeout(f, 200)); return responseText; - } catch (error: any) { + } catch (error: unknown) { console.error(error); sendToOutput(`result failed: ${JSON.stringify(error)}`); if (error instanceof vscode.LanguageModelError) { // Handle specific LLM errors let errorMessage = "VS Code Language Model Error: "; - + switch (error.code) { case vscode.LanguageModelError.NotFound().code: errorMessage += "Model not found. Please ensure GitHub Copilot is installed."; break; case vscode.LanguageModelError.NoPermissions().code: - errorMessage += "No permissions to use the language model. Please sign in to GitHub Copilot."; + errorMessage += + "No permissions to use the language model. Please sign in to GitHub Copilot."; break; case vscode.LanguageModelError.Blocked().code: errorMessage += "Request was blocked. The prompt may violate content policies."; @@ -211,12 +426,51 @@ class VsCodeLlmService implements AIService { default: errorMessage += error.message; } - + window.showErrorMessage(errorMessage); + } else if (error instanceof Error) { + // Handle the specific "model_not_supported" error + let isModelNotSupported = false; + let actualErrorMessage = error.message; + + // Check for model_not_supported in the error message + if ( + error.message.includes("model_not_supported") || + error.message.includes("Model is not supported") + ) { + isModelNotSupported = true; + } else { + // Try to parse the error message as JSON to check for nested error details + try { + // Extract JSON part from the error message if it exists + const jsonMatch = error.message.match(/\{.*\}/); + if (jsonMatch) { + const errorJson = JSON.parse(jsonMatch[0]); + if (errorJson.error && errorJson.error.code === "model_not_supported") { + isModelNotSupported = true; + // Use the actual error message from the LLM + if (errorJson.error.message) { + actualErrorMessage = errorJson.error.message; + } + } + } + } catch (parseError) { + // If parsing fails, continue with normal error handling + console.error("Failed to parse error JSON:", parseError); + } + } + + if (isModelNotSupported) { + window.showErrorMessage( + `Diffy Error: The selected language model doesn't support this type of request. Try switching to a different model in settings or use OpenAI instead. LLM Error: ${actualErrorMessage}`, + ); + } else { + window.showErrorMessage( + `Diffy Error: Failed to generate commit message. ${error.message}`, + ); + } } else { - window.showErrorMessage( - `Diffy Error: Failed to generate commit message. ${error.message || "Unknown error"}` - ); + window.showErrorMessage("Diffy Error: Failed to generate commit message. Unknown error"); } progress?.report({ diff --git a/src/service/WindowService.ts b/src/service/WindowService.ts index 644530c..f92171c 100644 --- a/src/service/WindowService.ts +++ b/src/service/WindowService.ts @@ -1,4 +1,4 @@ -import { ViewColumn, WebviewPanel, window } from "vscode"; +import { ViewColumn, type WebviewPanel, window } from "vscode"; export default class WindowService { static _instance: WindowService; @@ -24,15 +24,10 @@ export default class WindowService { } private createWebviewPanel() { - this.panel = window.createWebviewPanel( - "ExplainGitDiff", - "Explain Git Diff", - ViewColumn.Two, - { - enableScripts: false, - retainContextWhenHidden: true, - } - ); + this.panel = window.createWebviewPanel("ExplainGitDiff", "Explain Git Diff", ViewColumn.Two, { + enableScripts: false, + retainContextWhenHidden: true, + }); } private getWebviewContent(explain: string) { diff --git a/src/service/WorkspaceService.ts b/src/service/WorkspaceService.ts index 4cf3c85..d6d3dcd 100644 --- a/src/service/WorkspaceService.ts +++ b/src/service/WorkspaceService.ts @@ -1,23 +1,20 @@ -import { EventEmitter } from "events"; -import { window, workspace, WorkspaceFolder } from "vscode"; +import { EventEmitter } from "node:events"; +import { type WorkspaceFolder, window, workspace } from "vscode"; import { EventType } from "../@types/EventType"; import { CONSTANTS } from "../Constants"; export default class WorkspaceService extends EventEmitter { static _instance: WorkspaceService; - constructor() { - if (WorkspaceService._instance) { - return WorkspaceService._instance; - } + private constructor() { super(); /* Listening for changes to the workspace configuration. */ - workspace.onDidChangeConfiguration((e) => { + workspace.onDidChangeConfiguration((_e) => { this.emit(EventType.WORKSPACE_CHANGED); }); /* Listening for changes to the workspace configuration. */ - workspace.onDidChangeWorkspaceFolders((e) => { + workspace.onDidChangeWorkspaceFolders((_e) => { this.emit(EventType.WORKSPACE_CHANGED); }); } @@ -39,10 +36,7 @@ export default class WorkspaceService extends EventEmitter { * @returns A boolean value. */ checkAndWarnWorkSpaceExist(): boolean { - if ( - !workspace.workspaceFolders || - workspace.workspaceFolders.length === 0 - ) { + if (!workspace.workspaceFolders || workspace.workspaceFolders.length === 0) { this.showErrorMessage("Your are not in a Workspace"); return false; } @@ -55,10 +49,7 @@ export default class WorkspaceService extends EventEmitter { * @returns {string | null} The current workspace folder path. */ getCurrentWorkspaceUri(): string | null { - if ( - !workspace.workspaceFolders || - workspace.workspaceFolders.length === 0 - ) { + if (!workspace.workspaceFolders || workspace.workspaceFolders.length === 0) { return null; } return workspace.workspaceFolders[0].uri.fsPath; @@ -69,10 +60,7 @@ export default class WorkspaceService extends EventEmitter { * @returns {WorkspaceFolder | null} The first workspace folder in the workspace.workspaceFolders array. */ getCurrentWorkspace(): WorkspaceFolder | null { - if ( - !workspace.workspaceFolders || - workspace.workspaceFolders.length === 0 - ) { + if (!workspace.workspaceFolders || workspace.workspaceFolders.length === 0) { return null; } return workspace.workspaceFolders[0]; @@ -90,11 +78,21 @@ export default class WorkspaceService extends EventEmitter { return workspace.getConfiguration(CONSTANTS.extensionName); } + getAiServiceProvider() { + const value = String(this.getConfiguration().get("aiServiceProvider")); + return value || "openai"; + } + + getVsCodeLmModel() { + const value = String(this.getConfiguration().get("vscodeLmModel")); + return value || "auto"; + } + getOpenAIKey() { const value = String(this.getConfiguration().get("openAiKey")); if (!value) { this.showErrorMessage( - "Your OpenAI API Key is missing; kindly input it within the Diffy Settings section. You can generate a key by visiting the OpenAI website." + "Your OpenAI API Key is missing; kindly input it within the Diffy Settings section. You can generate a key by visiting the OpenAI website.", ); return null; } @@ -119,7 +117,7 @@ export default class WorkspaceService extends EventEmitter { : undefined; if (!value) { this.showErrorMessage( - "Instructions for AI are absent; please provide them within the Diffy Settings section." + "Instructions for AI are absent; please provide them within the Diffy Settings section.", ); return null; } @@ -140,6 +138,59 @@ export default class WorkspaceService extends EventEmitter { return value; } + getCommitMessageType() { + const value = String(this.getConfiguration().get("commitMessageType")); + return value || "conventional"; + } + + getCustomCommitPrompt() { + const value = this.getConfiguration().get("customCommitPrompt"); + if (typeof value === "string" && value.trim()) { + return value; + } + // Default custom template if not set + return `Generate a commit message for the following git diff. + +Requirements: +- Maximum subject length: {maxLength} characters +- Use imperative mood +- Be concise and clear{bodyInstructions} + +Return ONLY the commit message, no explanations.`; + } + + getIncludeCommitBody() { + const value = this.getConfiguration().get("includeCommitBody"); + return value === true; + } + + getExcludeFilesFromDiff(): string[] { + const value = this.getConfiguration().get("excludeFilesFromDiff"); + if (Array.isArray(value)) { + return value; + } + // Default exclusions + return [ + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + "*.jpg", + "*.png", + "*.gif", + "*.svg", + "*.ico", + "*.woff", + "*.woff2", + "*.ttf", + "*.eot", + ]; + } + + getMaxCommitMessageLength() { + const value = this.getConfiguration().get("maxCommitMessageLength"); + return typeof value === "number" ? value : 72; + } + /** * This function shows an error message * @param {string} msg - The message to display. diff --git a/src/test/runTest.ts b/src/test/runTest.ts index 27b3ceb..d569d87 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -1,23 +1,22 @@ -import * as path from 'path'; - -import { runTests } from '@vscode/test-electron'; +import * as path from "node:path"; +import { runTests } from "@vscode/test-electron"; async function main() { - try { - // The folder containing the Extension Manifest package.json - // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, "../../"); - // The path to test runner - // Passed to --extensionTestsPath - const extensionTestsPath = path.resolve(__dirname, './suite/index'); + // The path to test runner + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, "./suite/index"); - // Download VS Code, unzip it and run the integration test - await runTests({ extensionDevelopmentPath, extensionTestsPath }); - } catch (err) { - console.error('Failed to run tests'); - process.exit(1); - } + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch { + console.error("Failed to run tests"); + process.exit(1); + } } main(); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 4ca0ab4..ed67073 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,15 +1,16 @@ -import * as assert from 'assert'; +import * as assert from "node: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 vscode from "vscode"; + // import * as myExtension from '../../extension'; -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); +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)); - }); + test("Sample test", () => { + assert.strictEqual(-1, [1, 2, 3].indexOf(5)); + assert.strictEqual(-1, [1, 2, 3].indexOf(0)); + }); }); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 7029e38..4f4a65e 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -1,38 +1,41 @@ -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; +import * as path from "node:path"; +import { glob } from "glob"; + +import Mocha = require("mocha"); export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd', - color: true - }); + // Create the mocha test + const mocha = new Mocha({ + ui: "tdd", + color: true, + }); - const testsRoot = path.resolve(__dirname, '..'); + const testsRoot = path.resolve(__dirname, ".."); - return new Promise((c, e) => { - glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { - if (err) { - return e(err); - } + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err: Error | null, files: string[]) => { + if (err) { + return e(err); + } - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + // Add files to the test suite + files.forEach((f: string) => { + mocha.addFile(path.resolve(testsRoot, f)); + }); - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - console.error(err); - e(err); - } - }); - }); + try { + // Run the mocha test + mocha.run((failures: number) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err instanceof Error ? err : new Error(String(err))); + } + }); + }); } diff --git a/tsconfig.json b/tsconfig.json index 965a7b4..d2d3666 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,15 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "ES2020", - "lib": [ - "ES2020" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */, + /* Additional Checks */ + "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, + "useUnknownInCatchVariables": true /* Type caught variables as 'unknown' instead of 'any' */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + } } diff --git a/webpack.config.js b/webpack.config.js index 37d7024..b9abb21 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,31 +1,29 @@ //@ts-check -'use strict'; - -const path = require('path'); +const path = require("node:path"); //@ts-check /** @typedef {import('webpack').Configuration} WebpackConfig **/ /** @type WebpackConfig */ const extensionConfig = { - target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ - mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + target: "node", // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ + mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') - entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ + entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ - path: path.resolve(__dirname, 'dist'), - filename: 'extension.js', - libraryTarget: 'commonjs2' + path: path.resolve(__dirname, "dist"), + filename: "extension.js", + libraryTarget: "commonjs2", }, externals: { - vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ // modules added here also need to be added in the .vscodeignore file }, resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader - extensions: ['.ts', '.js'] + extensions: [".ts", ".js"], }, module: { rules: [ @@ -34,15 +32,15 @@ const extensionConfig = { exclude: /node_modules/, use: [ { - loader: 'ts-loader' - } - ] - } - ] + loader: "ts-loader", + }, + ], + }, + ], }, - devtool: 'nosources-source-map', + devtool: "nosources-source-map", infrastructureLogging: { level: "log", // enables logging required for problem matchers }, }; -module.exports = [ extensionConfig ]; \ No newline at end of file +module.exports = [extensionConfig];