├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── fonts.woff ├── logo.png ├── readme │ ├── chat-title-dark.gif │ ├── commit-message-dark.gif │ ├── inline-chat-dark.gif │ ├── inline-completion-dark.gif │ ├── panel-chat-dark.gif │ ├── quick-chat-dark.gif │ ├── token-usage-dark.gif │ └── voice-chat-dark.gif └── walkthrough.md ├── build ├── env-setup.mjs └── post-build.mjs ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── src ├── commands │ ├── commit-message.ts │ ├── configure-model.ts │ ├── github-sign-in.ts │ └── status-icon-menu.ts ├── completion.ts ├── constants.ts ├── events.ts ├── extension.ts ├── inline-chat.ts ├── interfaces.ts ├── lazy-load.ts ├── logger.ts ├── models.ts ├── panel-chat.ts ├── prompts │ ├── commands │ │ └── commit-message.tsx │ ├── inline-chat.tsx │ ├── jsx-utilities.tsx │ └── panel-chat.tsx ├── providers │ ├── anthropic.ts │ ├── azure.ts │ ├── generic.ts │ ├── google.ts │ ├── groq.ts │ ├── index.ts │ ├── mistral-ai.ts │ └── openai.ts ├── session.ts ├── startup.ts ├── status-icon.ts ├── storage.ts ├── tokenizers.ts ├── utilities.ts └── variables.tsx ├── tokenizers ├── cl100k_base.json └── codestral-v0.1.json ├── tsconfig.json └── webpack.config.mjs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build VS Code Extension 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | name: "Build VS Code Extension" 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | platform: 19 | [ 20 | "win32-arm64", 21 | "win32-x64", 22 | "linux-arm64", 23 | "linux-x64", 24 | "linux-armhf", 25 | "alpine-x64", 26 | "darwin-x64", 27 | "darwin-arm64", 28 | "alpine-arm64", 29 | ] 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: "20" 39 | 40 | - name: Install dependencies 41 | run: npm install 42 | 43 | - name: Checking Linting 44 | run: npm run lint 45 | 46 | - name: Compile Extension 47 | run: npm run package -- --target ${{ matrix.platform }} 48 | 49 | - name: Package extension 50 | run: npx vsce package --target ${{ matrix.platform }} --out "flexpilot-${{ matrix.platform }}.vsix" 51 | 52 | - name: Upload extension vsix as artifact 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: "flexpilot-${{ matrix.platform }}.vsix" 56 | path: "flexpilot-${{ matrix.platform }}.vsix" 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release VS Code Extension 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | release-job: 12 | name: "Release VS Code Extension" 13 | runs-on: ubuntu-latest 14 | environment: production 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | platform: 19 | [ 20 | "win32-arm64", 21 | "win32-x64", 22 | "linux-arm64", 23 | "linux-x64", 24 | "linux-armhf", 25 | "alpine-x64", 26 | "darwin-x64", 27 | "darwin-arm64", 28 | "alpine-arm64", 29 | ] 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: "20" 39 | 40 | - name: Verify version match 41 | run: | 42 | # Get version from package.json 43 | PKG_VERSION=$(node -p "require('./package.json').version") 44 | 45 | # Get release version 46 | RELEASE_VERSION=${GITHUB_REF#refs/tags/} 47 | 48 | # Validate release tag format 49 | if ! [[ $RELEASE_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 50 | echo "Error: Invalid release tag format!" 51 | echo "Release tag must follow the format: 1.2.3 (MAJOR.MINOR.PATCH)" 52 | echo "Got: $RELEASE_VERSION" 53 | exit 1 54 | fi 55 | 56 | # Validate package.json version format 57 | if ! [[ $PKG_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 58 | echo "Error: Invalid package.json version format!" 59 | echo "Version must follow SemVer format: 1.2.3 (MAJOR.MINOR.PATCH)" 60 | echo "Got: $PKG_VERSION" 61 | exit 1 62 | fi 63 | 64 | echo "Package.json version: $PKG_VERSION" 65 | echo "Release version: $RELEASE_VERSION" 66 | 67 | # Check if package.json version matches release version 68 | if [ "$PKG_VERSION" != "$RELEASE_VERSION" ]; then 69 | echo "Error: Version mismatch!" 70 | echo "package.json version ($PKG_VERSION) does not match release version ($RELEASE_VERSION)" 71 | exit 1 72 | fi 73 | 74 | - name: Install dependencies 75 | run: npm install 76 | 77 | - name: Checking Linting 78 | run: npm run lint 79 | 80 | - name: Compile Extension 81 | run: npm run package -- --target ${{ matrix.platform }} 82 | 83 | - name: Package extension 84 | run: npx vsce package --target ${{ matrix.platform }} --out "flexpilot-${{ matrix.platform }}.vsix" 85 | 86 | - name: Upload extension vsix as artifact 87 | uses: actions/upload-artifact@v4 88 | with: 89 | name: "flexpilot-${{ matrix.platform }}.vsix" 90 | path: "flexpilot-${{ matrix.platform }}.vsix" 91 | 92 | - name: Upload Release Assets 93 | uses: softprops/action-gh-release@v2 94 | with: 95 | files: "flexpilot-${{ matrix.platform }}.vsix" 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | 99 | - name: Release to VS Code Marketplace 100 | uses: nick-fields/retry@v3 101 | with: 102 | timeout_minutes: 5 103 | max_attempts: 6 104 | retry_on: error 105 | retry_wait_seconds: 10 106 | command: npx vsce publish --packagePath "flexpilot-${{ matrix.platform }}.vsix" -p ${{ secrets.VSCODE_MARKETPLACE_TOKEN }} 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | .DS_Store 6 | launch_folder 7 | types/ 8 | extension.vsix 9 | extension.hash -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npx lint-staged 3 | 4 | if git diff --cached --name-only | grep -qE "^(package\.json|package-lock\.json)$"; then 5 | echo "Error: Changes to package.json or package-lock.json are not allowed." 6 | exit 1 7 | fi 8 | 9 | echo "Completed Pre Commit Hooks ..." -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "${workspaceFolder}/launch_folder", 14 | "--extensionDevelopmentPath=${workspaceFolder}", 15 | "--disable-extensions", 16 | "--enable-proposed-api=flexpilot.flexpilot-vscode-extension" 17 | ], 18 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 19 | "env": { 20 | "FLEXPILOT_DEV_MODE": "true" 21 | }, 22 | "preLaunchTask": "${defaultBuildTask}" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { "out": true }, 3 | "files.exclude": { "out": false }, 4 | "typescript.tsc.autoDetect": "off", 5 | "prettier.prettierPath": "./node_modules/prettier", 6 | "editor.formatOnSave": true, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", 9 | "editor.indentSize": "tabSize", 10 | "editor.insertSpaces": true, 11 | "editor.tabSize": 2, 12 | "editor.codeActionsOnSave": { 13 | "source.organizeImports": "explicit" 14 | } 15 | }, 16 | "[javascript]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode", 18 | "editor.indentSize": "tabSize", 19 | "editor.insertSpaces": true, 20 | "editor.tabSize": 2, 21 | "editor.codeActionsOnSave": { 22 | "source.organizeImports": "explicit" 23 | } 24 | }, 25 | "[typescriptreact]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode", 27 | "editor.indentSize": "tabSize", 28 | "editor.insertSpaces": true, 29 | "editor.tabSize": 2, 30 | "editor.codeActionsOnSave": { 31 | "source.organizeImports": "explicit" 32 | } 33 | }, 34 | "cSpell.words": [ 35 | "Codestral", 36 | "davinci", 37 | "flexpilot", 38 | "Groq", 39 | "groqcloud", 40 | "ISCM", 41 | "Kwargs", 42 | "machineid", 43 | "mistralai", 44 | "NOSONAR", 45 | "softprops", 46 | "uuidv" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | node_modules/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | webpack.config.js 8 | vsc-extension-quickstart.md 9 | **/tsconfig.json 10 | **/.eslintrc.json 11 | **/*.map 12 | **/*.ts 13 | **/.vscode-test.* 14 | .github 15 | launch_folder 16 | types 17 | build/** 18 | tokenizers/** 19 | .husky/** 20 | eslint.config.mjs -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | contact@flexpilot.ai. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Flexpilot 2 | 3 | Thank you for your interest in contributing to Flexpilot! We welcome contributions from the community and are grateful for any time you're willing to contribute. 4 | 5 | ## Current Licensing 6 | 7 | Flexpilot is currently licensed under the GNU General Public License v3.0 (GPLv3). While contributing under this license, please ensure you understand its terms. However, please note that by signing our CLA, you grant us the flexibility to potentially change the license in the future. 8 | 9 | ## Getting Started 10 | 11 | 1. **Found a bug?** 12 | 13 | - Check our [issue tracker](https://github.com/flexpilot-ai/flexpilot/issues) to see if it's already reported 14 | - If not, create a new issue with detailed information about how to reproduce it 15 | 16 | 2. **Want to contribute?** 17 | 18 | - Visit our [project board](https://github.com/orgs/flexpilot-ai/projects/3) to find tasks and features we need help with 19 | - Feel free to pick up any open issues or suggest new features 20 | 21 | ## Contributor License Agreement 22 | 23 | By contributing to Flexpilot, you agree to the following terms of our Contributor License Agreement: 24 | 25 | ### Terms of Agreement 26 | 27 | For good and valuable consideration, receipt of which I acknowledge, I agree to the following terms and conditions: 28 | 29 | 1. **Definitions:** 30 | 31 | - "Code" means the computer software code, whether in human-readable or machine-executable form, that I submit to Flexpilot. 32 | - "Submit" means any form of electronic, verbal, or written communication sent to Flexpilot, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems. 33 | - "Contribution" means any Code, documentation, or other original work of authorship that is Submitted by me to Flexpilot. 34 | 35 | 2. **Grant of Rights:** 36 | 37 | - I grant to Flexpilot and to recipients of software distributed by Flexpilot a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license to use, copy, modify, and distribute my Contributions. 38 | - I understand and agree that my Contributions may be included in commercial products by Flexpilot. 39 | - I grant Flexpilot the right to change the license of my Contributions without additional permission, even though the project currently uses the GNU GPLv3 license. 40 | 41 | 3. **Originality of Work:** 42 | 43 | - I represent that each of my Contributions is entirely my original work. 44 | - Where I wish to Submit materials that are not my original work, I will clearly identify the source and any license or other restriction of which I am personally aware. 45 | 46 | 4. **Moral Rights:** 47 | 48 | - To the fullest extent permitted under applicable law, I waive all moral rights in my Contributions in favor of Flexpilot. 49 | 50 | 5. **Third Party Rights:** 51 | 52 | - I represent that I am legally entitled to grant the above license. 53 | - If my employer(s) has rights to intellectual property that I create that includes my Contributions, I represent that I have received permission to make Contributions on behalf of that employer. 54 | 55 | 6. **Future Changes:** 56 | - I acknowledge that Flexpilot is not obligated to use my Contributions and may decide to include any Contribution Flexpilot considers appropriate. 57 | - I understand that while the project is currently under GNU GPLv3, my contributions may be used in a commercial product and that Flexpilot reserves the right to change the license or move to a proprietary license in the future. 58 | 59 | ## Development Workflow 60 | 61 | 1. Fork the repository, clone the fork locally, and create a new branch. 62 | 2. Run the `npm install` command to install the dependencies. 63 | 3. Open the project in VS Code and run the extension in Debug mode. 64 | 4. Make your changes and check for errors by running `npm run lint`. 65 | 5. Format your code by running `npm run format`. 66 | 6. Push your changes and create a Pull Request against our main repository. 67 | 68 | ## Code of Conduct 69 | 70 | By participating in this project, you agree to abide by our Code of Conduct (see Code of Conduct.md). 71 | 72 | ## Questions? 73 | 74 | Don't hesitate to write to us at [contact@flexpilot.ai](mailto:contact@flexpilot.ai) if you have any questions or need help with anything. 75 | 76 | ## License Notice 77 | 78 | While Flexpilot is currently licensed under GNU GPLv3, by contributing to this project, you acknowledge and agree that your contributions will be licensed under the terms of our CLA, which permits Flexpilot to potentially relicense the code in the future. 79 | 80 | --- 81 | 82 | Thank you for contributing to Flexpilot! 🚀 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flexpilot AI - Your Open-Source AI Assistant 2 | 3 | > _"Open-Source, Native and a True GitHub Copilot Alternative for VS Code"_ 4 | 5 | > [!WARNING] 6 | > The **Flexpilot VS Code extension** is no longer actively maintained. We will still try to address any issues or pull requests, but will not be adding new features. 7 | > 8 | > We have created `Flexpilot IDE` as its successor - a fork of VS Code with the Flexpilot extension pre-installed, along with powerful new features such as multi-file editing, an online web IDE, and more. 9 | > 10 | > 🚀 Click [here](https://ide.flexpilot.ai/?folder=web-fs://github/flexpilot-ai/flexpilot-ide/main) to experience it instantly Online. 11 | > 12 | > 📥 Click [here](https://flexpilot.ai/docs/getting-started#downloading-the-ide) to download the latest version of Flexpilot Desktop IDE for the best experience. 13 | 14 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/flexpilot-ai/vscode-extension/build.yml) 15 | [![License](https://img.shields.io/badge/license-GNU%20GPLv3-blue.svg)](LICENSE) 16 | ![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/flexpilot.flexpilot-vscode-extension) 17 | [![Star on GitHub](https://img.shields.io/github/stars/flexpilot-ai/vscode-extension?style=social)](https://github.com/flexpilot-ai/vscode-extension) 18 | 19 | Flexpilot is your gateway to truly flexible AI-powered development. Unlike other AI assistants, Flexpilot puts **you** in control, letting you use your preferred AI providers and models directly in VS Code. Native integration, unparalleled flexibility, and open-source freedom - all in one powerful package. 20 | 21 | ## 🚀 Getting Started 22 | 23 | 1. Install Flexpilot from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=flexpilot.flexpilot-vscode-extension) and restart VS Code once installation is complete. 24 | 2. Configure your preferred Language model providers as explained [here](https://docs.flexpilot.ai/configuration.html) in the official documentation. 25 | 3. Start coding with AI-powered completions, chat, and more! 26 | 27 | ## ✨ Why Flexpilot? 28 | 29 | - 🎯 **100% Native VS Code Experience** - No clunky webviews, just pure coding bliss 30 | - 🔑 **Your Keys, Your Control** - Use your own API keys with top AI providers 31 | - 🎨 **Ultimate Flexibility** - Mix and match AI models for different tasks 32 | - 🌟 **Open Source Freedom** - Fully transparent, customizable, and community-driven 33 | - 💎 **GitHub Copilot Compatible** - Leverage your existing skills seamlessly 34 | 35 | ## 🛠️ Supercharged Features 36 | 37 | ### 🤖 Code Completions 38 | 39 | Effortlessly code with **AI-powered autocomplete** that provides context-aware suggestions and natural language guidance tailored to your project. 40 | 41 | ![Inline Completions](./assets/readme/inline-completion-dark.gif) 42 | 43 | ### 💬 Panel Chat 44 | 45 | Experience **context-aware, interactive AI conversations** directly within your VSCode workspace. Flexpilot’s panel chat keeps you focused on problem-solving without ever leaving your codebase. 46 | 47 | ![Panel Chat](./assets/readme/panel-chat-dark.gif) 48 | 49 | ### ✍️ Inline Chat 50 | 51 | Refactor, debug, or gain instant clarity with **Inline Chat**. Whether you need error handling suggestions or code explanations, Flexpilot’s inline chat feature lets you make changes directly in your editor. 52 | 53 | ![Inline Chat](./assets/readme/inline-chat-dark.gif) 54 | 55 | ### ⚡ Quick Chat 56 | 57 | Stay in the zone with **Quick Chat** – instant answers from your AI assistant with a single shortcut. No more breaking your workflow to find answers; just quick solutions at your fingertips. 58 | 59 | ![Quick Chat](./assets/readme/quick-chat-dark.gif) 60 | 61 | ### 🎯 Smart Variables 62 | 63 | Get precision in your AI interactions with **Smart Variables**. Flexpilot references elements from your code and editor data, allowing for more tailored and relevant assistance. 64 | 65 | ![Smart Variables](./assets/readme/panel-chat-dark.gif) 66 | 67 | ### 🎙️ Voice Chat 68 | 69 | Have a question? Just ask! **Voice Chat** enables you to speak directly to your AI assistant and receive code suggestions in real time, allowing you to stay hands-free while coding. 70 | 71 | ![Voice Chat](./assets/readme/voice-chat-dark.gif) 72 | 73 | ### 📄 Dynamic Chat Titles 74 | 75 | Maintain clarity in your AI-assisted conversations with **Dynamic Chat Titles**. Flexpilot automatically generates concise, relevant titles for each chat, making it easy to revisit previous conversations. 76 | 77 | ![Dynamic Chat Titles](./assets/readme/chat-title-dark.gif) 78 | 79 | ### 💻 Commit Messages 80 | 81 | Simplify your workflow with **AI-generated commit messages** and PR descriptions. Flexpilot crafts detailed and context-aware commit messages to make your code contributions clearer and more descriptive. 82 | 83 | ![Commit Messages](./assets/readme/commit-message-dark.gif) 84 | 85 | ### 📊 Token Usage Insights 86 | 87 | Gain transparency with **Token Usage Insights**. Track real-time token consumption across all AI interactions, helping you manage your usage and costs more effectively. 88 | 89 | ![Token Usage Insights](./assets/readme/token-usage-dark.gif) 90 | 91 | ## 🎯 Supported AI Providers 92 | 93 | - Anthropic 94 | - OpenAI 95 | - Azure OpenAI 96 | - Groq 97 | - Google Gemini 98 | - Mistral AI 99 | - Ollama 100 | - Anyscale 101 | - KoboldCpp 102 | - text-gen-webui 103 | - FastChat 104 | - LocalAI 105 | - llama-cpp-python 106 | - TensorRT-LLM 107 | - vLLM 108 | - _Any many more!_ 109 | 110 | ## 🗺️ Roadmap 111 | 112 | - ✨ **Multi-File Chat Edits** - Seamlessly manage AI-assisted changes across multiple files. 113 | - 🧑‍💻 **@Workspace Agent** - Contextual AI support for your entire project workspace. 114 | - 🔌 **Extended Copilot Extensions** - Integrate and leverage [GitHub Copilot Extensions](https://github.com/marketplace?type=apps&copilot_app=true). 115 | - _Any many more!_ 116 | 117 | ## 🤝 Open Source Community 118 | 119 | Flexpilot is proudly open source under the GNU GPLv3 license. We believe in: 120 | 121 | - 🌟 **Community-First Development** 122 | - 🛠️ **Transparent Architecture** 123 | - 🤝 **Collaborative Innovation** 124 | - 🚀 **Continuous Improvement** 125 | 126 | ## 🤝 Contributing 127 | 128 | We love contributions! Whether it's: 129 | 130 | - 🐛 Bug Reports 131 | - ✨ Feature Requests 132 | - 📝 Documentation 133 | - 💻 Code Contributions 134 | 135 | Check our [Contributing Guide](CONTRIBUTING.md) to get started! 136 | 137 | ## 📜 License 138 | 139 | Flexpilot is open source under the [GNU GPLv3 License](LICENSE). 140 | 141 | ## 🌟 Star Us on GitHub! 142 | 143 | If you love Flexpilot, show your support [here](https://github.com/flexpilot-ai/vscode-extension) by starring us on GitHub! Every star motivates us to make Flexpilot even better. 144 | 145 |

146 | Made with ❤️ by developers, for developers 147 |
148 | © 2024 Flexpilot AI Inc. 149 |

150 | -------------------------------------------------------------------------------- /assets/fonts.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/fonts.woff -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/logo.png -------------------------------------------------------------------------------- /assets/readme/chat-title-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/readme/chat-title-dark.gif -------------------------------------------------------------------------------- /assets/readme/commit-message-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/readme/commit-message-dark.gif -------------------------------------------------------------------------------- /assets/readme/inline-chat-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/readme/inline-chat-dark.gif -------------------------------------------------------------------------------- /assets/readme/inline-completion-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/readme/inline-completion-dark.gif -------------------------------------------------------------------------------- /assets/readme/panel-chat-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/readme/panel-chat-dark.gif -------------------------------------------------------------------------------- /assets/readme/quick-chat-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/readme/quick-chat-dark.gif -------------------------------------------------------------------------------- /assets/readme/token-usage-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/readme/token-usage-dark.gif -------------------------------------------------------------------------------- /assets/readme/voice-chat-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexpilot-ai/vscode-extension/360af0191b369dceb714b5838ff745d201260f3f/assets/readme/voice-chat-dark.gif -------------------------------------------------------------------------------- /assets/walkthrough.md: -------------------------------------------------------------------------------- 1 |
2 | Description of the image 3 |

If you're uncertain about the current step, please refer to the official documentation here for more information.

4 |
5 | 6 |
7 | 8 |

9 | Made with ❤️ by developers, for developers 10 |
11 | © 2024 Flexpilot AI Inc. 12 |

13 | -------------------------------------------------------------------------------- /build/env-setup.mjs: -------------------------------------------------------------------------------- 1 | // import necessary modules 2 | import * as fs from "fs"; 3 | import { Octokit } from "octokit"; 4 | import * as path from "path"; 5 | import * as semver from "semver"; 6 | 7 | // Create a new instance of the Octokit client 8 | const github = new Octokit(); 9 | 10 | // Read the package.json file to get the version number 11 | let packageJson = JSON.parse(await fs.readFileSync("package.json", "utf-8")); 12 | 13 | // Get the vscode release version 14 | const version = semver.parse(packageJson.version); 15 | 16 | // Check if the branch exists in the vscode repository 17 | let vscodeBranch = `release/${version.major}.${version.minor}`; 18 | try { 19 | await github.rest.repos.getBranch({ 20 | owner: "microsoft", 21 | repo: "vscode", 22 | branch: vscodeBranch, 23 | }); 24 | // Log the branch being used 25 | console.log(`[INFO] Fetching from branch: ${vscodeBranch}`); 26 | } catch { 27 | // If the branch does not exist, use the main branch 28 | console.warn(`[WARN] Branch does not exist`); 29 | vscodeBranch = "main"; 30 | console.warn(`[WARN] Using main branch instead`); 31 | } 32 | 33 | console.log("Fetching from branch:", vscodeBranch); 34 | 35 | // Get the list of proposed APIs from the vscode repository 36 | const vscodeDtsFiles = await github.rest.repos.getContent({ 37 | owner: "microsoft", 38 | repo: "vscode", 39 | ref: vscodeBranch, 40 | path: "src/vscode-dts", 41 | }); 42 | 43 | // Filter out the proposed APIs from the list of files 44 | const proposedApis = vscodeDtsFiles.data 45 | .map((item) => item.name) 46 | .filter((item) => item.startsWith("vscode.proposed.")) 47 | .filter((item) => item.endsWith(".d.ts")) 48 | .map((item) => item.replace("vscode.proposed.", "")) 49 | .map((item) => item.replace(".d.ts", "")); 50 | 51 | // Update the package.json file 52 | packageJson.enabledApiProposalsOriginal = proposedApis; 53 | packageJson.engines = {}; 54 | packageJson.engines.vscode = `${version.major}.${version.minor}.x`; 55 | 56 | // Write the updated package.json file 57 | await fs.writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); 58 | 59 | // Get the list of git APIs from the vscode repository 60 | const gitDtsFiles = await github.rest.repos.getContent({ 61 | owner: "microsoft", 62 | repo: "vscode", 63 | ref: vscodeBranch, 64 | path: "extensions/git/src/api", 65 | }); 66 | 67 | // Filter only ts declaration files 68 | const declarationFiles = vscodeDtsFiles.data 69 | .filter((item) => item.name.endsWith(".d.ts")) 70 | .concat(gitDtsFiles.data.filter((item) => item.name.endsWith(".d.ts"))); 71 | 72 | // Create the types directory if it doesn't exist 73 | if (!fs.existsSync("./types")) { 74 | fs.mkdirSync("./types", { recursive: true }); 75 | } 76 | 77 | // Download the .d.ts files from the vscode repository 78 | for (const element of declarationFiles) { 79 | const item = element; 80 | const fileName = path.basename(item.download_url); 81 | const filePath = path.join("./types", fileName); 82 | const response = await fetch(item.download_url); 83 | const buffer = await response.arrayBuffer(); 84 | fs.writeFileSync(filePath, Buffer.from(buffer)); 85 | } 86 | -------------------------------------------------------------------------------- /build/post-build.mjs: -------------------------------------------------------------------------------- 1 | import { Command, Option } from "commander"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import extensionConfig from "../webpack.config.mjs"; 5 | 6 | const targetMapping = { 7 | "win32-x64": "node-napi.win32-x64-msvc.node", 8 | "win32-arm64": "node-napi.win32-arm64-msvc.node", 9 | "linux-x64": "node-napi.linux-x64-gnu.node", 10 | "linux-arm64": "node-napi.linux-arm64-gnu.node", 11 | "linux-armhf": "node-napi.linux-arm-gnueabihf.node", 12 | "alpine-x64": "node-napi.linux-x64-musl.node", 13 | "darwin-x64": "node-napi.darwin-x64.node", 14 | "darwin-arm64": "node-napi.darwin-arm64.node", 15 | "alpine-arm64": "node-napi.linux-arm64-musl.node", 16 | }; 17 | 18 | const args = new Command() 19 | .addOption( 20 | new Option("--target [platform]", "Specify the target VS Code platform") 21 | .choices(Object.keys(targetMapping)) 22 | .makeOptionMandatory(), 23 | ) 24 | .parse() 25 | .opts(); 26 | 27 | const outDir = extensionConfig[0].output.path; 28 | for (const file of fs.readdirSync(outDir)) { 29 | if (file === targetMapping[args.target]) { 30 | continue; 31 | } else if (file.startsWith("node-napi")) { 32 | console.log(`Deleting file: ${file}`); 33 | fs.unlinkSync(path.join(outDir, file)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import pluginJs from "@eslint/js"; 2 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 3 | import globals from "globals"; 4 | import tseslint from "typescript-eslint"; 5 | 6 | export default [ 7 | { ignores: ["node_modules/*", "types/*", "out/*", "launch_folder/*"] }, 8 | { files: ["**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}"] }, 9 | { languageOptions: { globals: globals.node } }, 10 | pluginJs.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | eslintPluginPrettierRecommended, 13 | { 14 | ignores: ["node_modules/**", "build/**"], 15 | rules: { 16 | "no-restricted-imports": [ 17 | "error", 18 | { 19 | paths: [ 20 | { 21 | name: "fs", 22 | message: "Please use vscode.workspace.fs", 23 | }, 24 | { 25 | name: "fs/promises", 26 | message: "Please use vscode.workspace.fs", 27 | }, 28 | ], 29 | }, 30 | ], 31 | }, 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "flexpilot", 3 | "name": "flexpilot-vscode-extension", 4 | "displayName": "Flexpilot", 5 | "description": "Open-Source, Native and a True GitHub Copilot Alternative for VS Code", 6 | "version": "1.97.0", 7 | "icon": "assets/logo.png", 8 | "license": "GPL-3.0-only", 9 | "pricing": "Free", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/flexpilot-ai/vscode-extension" 13 | }, 14 | "categories": [ 15 | "AI", 16 | "Chat", 17 | "Data Science", 18 | "Education", 19 | "Testing", 20 | "Programming Languages", 21 | "Machine Learning" 22 | ], 23 | "keywords": [ 24 | "flexpilot", 25 | "free", 26 | "open", 27 | "open-source", 28 | "ai", 29 | "openai", 30 | "codex", 31 | "pilot", 32 | "snippets", 33 | "documentation", 34 | "autocomplete", 35 | "intellisense", 36 | "refactor", 37 | "javascript", 38 | "python", 39 | "typescript", 40 | "php", 41 | "go", 42 | "golang", 43 | "ruby", 44 | "c++", 45 | "c#", 46 | "java", 47 | "kotlin", 48 | "co-pilot" 49 | ], 50 | "activationEvents": [ 51 | "onStartupFinished" 52 | ], 53 | "main": "./out/extension.js", 54 | "scripts": { 55 | "compile": "webpack", 56 | "watch": "webpack --watch", 57 | "package": "webpack --mode production --devtool hidden-source-map && node ./build/post-build.mjs", 58 | "compile-tests": "tsc -p . --outDir out", 59 | "watch-tests": "tsc -p . -w --outDir out", 60 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 61 | "test": "vscode-test", 62 | "format": "prettier --write \"**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md}\"", 63 | "lint": "eslint --config=eslint.config.mjs --fix", 64 | "prepare": "concurrently \"husky\" \"node ./build/env-setup.mjs\"" 65 | }, 66 | "devDependencies": { 67 | "@eslint/js": "^9.13.0", 68 | "@types/markdown-it": "^14.1.1", 69 | "@types/mocha": "^10.0.6", 70 | "@types/node": "^18.19.61", 71 | "@types/turndown": "^5.0.5", 72 | "@types/uuid": "^10.0.0", 73 | "@vscode/test-cli": "^0.0.4", 74 | "@vscode/vsce": "github:mohankumarelec/vscode-vsce#npm", 75 | "commander": "^12.1.0", 76 | "concurrently": "^9.0.1", 77 | "eslint": "^9.13.0", 78 | "eslint-config-prettier": "^9.1.0", 79 | "eslint-plugin-prettier": "^5.2.1", 80 | "globals": "^15.11.0", 81 | "husky": "^9.1.6", 82 | "lint-staged": "^15.2.10", 83 | "node-loader": "^2.0.0", 84 | "octokit": "^4.0.2", 85 | "prettier": "3.3.3", 86 | "ts-loader": "^9.5.1", 87 | "typescript": "^5.5.4", 88 | "typescript-eslint": "^8.12.2", 89 | "webpack": "^5.89.0", 90 | "webpack-cli": "^5.1.4" 91 | }, 92 | "contributes": { 93 | "walkthroughs": [ 94 | { 95 | "id": "Flexpilot", 96 | "title": "Flexpilot AI", 97 | "description": "This is a walkthrough for getting started with Flexpilot.", 98 | "icon": "assets/logo.png", 99 | "steps": [ 100 | { 101 | "id": "signin", 102 | "title": "GitHub Sign In", 103 | "description": "Sign in with GitHub to use Flexpilot, your AI pair programmer.\n[Sign In](command:flexpilot.github.signin)", 104 | "media": { 105 | "markdown": "assets/walkthrough.md" 106 | }, 107 | "completionEvents": [ 108 | "onContext:flexpilot:walkthroughSignin" 109 | ] 110 | }, 111 | { 112 | "id": "configureModel", 113 | "title": "Configure Model", 114 | "description": "Configure the language model provider for Flexpilot.\n[Configure](command:flexpilot.configureModel)", 115 | "media": { 116 | "markdown": "assets/walkthrough.md" 117 | }, 118 | "completionEvents": [ 119 | "onContext:flexpilot:walkthroughConfigureModel" 120 | ] 121 | }, 122 | { 123 | "id": "panelChat", 124 | "title": "Start Chatting", 125 | "description": "Chat with Flexpilot in the panel chat view with the configured model.\n[Start Chat](command:workbench.action.chat.open?%7B%22query%22%3A%22What%20model%20do%20you%20use%3F%22%7D)", 126 | "media": { 127 | "markdown": "assets/walkthrough.md" 128 | }, 129 | "completionEvents": [ 130 | "onContext:flexpilot:walkthroughPanelChat" 131 | ] 132 | } 133 | ] 134 | } 135 | ], 136 | "menus": {}, 137 | "configuration": { 138 | "type": "object", 139 | "title": "Flexpilot", 140 | "properties": { 141 | "flexpilot.completions.debounceWait": { 142 | "type": "number", 143 | "minimum": 10, 144 | "default": 100, 145 | "description": "The time in milliseconds to wait before triggering a completion." 146 | }, 147 | "flexpilot.completions.contextPrefixWeight": { 148 | "type": "number", 149 | "maximum": 0.95, 150 | "minimum": 0.05, 151 | "default": 0.85, 152 | "description": "The proportion of prefix to suffix tokens in the context for completions." 153 | }, 154 | "flexpilot.completions.temperature": { 155 | "type": "number", 156 | "maximum": 1, 157 | "minimum": 0, 158 | "default": 0.1, 159 | "description": "The randomness of completions, with 0.0 being deterministic and 1.0 being random." 160 | }, 161 | "flexpilot.completions.maxTokenUsage": { 162 | "type": "number", 163 | "maximum": 128000, 164 | "minimum": 500, 165 | "default": 4000, 166 | "description": "Maximum number of tokens (prompt + output) allowed for generating completions. This will help control the usage cost." 167 | }, 168 | "flexpilot.panelChat.temperature": { 169 | "type": "number", 170 | "maximum": 1, 171 | "minimum": 0, 172 | "default": 0.1, 173 | "description": "The randomness of panel chat, with 0.0 being deterministic and 1.0 being random." 174 | }, 175 | "flexpilot.panelChat.showTokenUsage": { 176 | "type": "boolean", 177 | "default": true, 178 | "description": "Whether to show token usage in panel chat." 179 | }, 180 | "flexpilot.inlineChat.temperature": { 181 | "type": "number", 182 | "maximum": 1, 183 | "minimum": 0, 184 | "default": 0.1, 185 | "description": "The randomness of inline chat, with 0.0 being deterministic and 1.0 being random." 186 | }, 187 | "flexpilot.inlineChat.showTokenUsage": { 188 | "type": "boolean", 189 | "default": true, 190 | "description": "Whether to show token usage in inline chat." 191 | }, 192 | "flexpilot.chatSuggestions.temperature": { 193 | "type": "number", 194 | "maximum": 1, 195 | "minimum": 0, 196 | "default": 0.1, 197 | "description": "The randomness of chat suggestions, with 0.0 being deterministic and 1.0 being random." 198 | }, 199 | "flexpilot.chatTitle.temperature": { 200 | "type": "number", 201 | "maximum": 1, 202 | "minimum": 0, 203 | "default": 0.1, 204 | "description": "The randomness of chat title, with 0.0 being deterministic and 1.0 being random." 205 | }, 206 | "flexpilot.gitCommitMessage.temperature": { 207 | "type": "number", 208 | "maximum": 1, 209 | "minimum": 0, 210 | "default": 0.1, 211 | "description": "The randomness of GIT commit messages, with 0.0 being deterministic and 1.0 being random." 212 | } 213 | } 214 | }, 215 | "chatParticipants": [ 216 | { 217 | "id": "flexpilot.panel.default", 218 | "name": "flexpilot", 219 | "fullName": "Flexpilot", 220 | "description": "Ask Flexpilot or type / for commands", 221 | "isDefault": true, 222 | "locations": [ 223 | "panel" 224 | ] 225 | }, 226 | { 227 | "id": "flexpilot.editor.default", 228 | "name": "flexpilot", 229 | "fullName": "Flexpilot", 230 | "description": "Ask Flexpilot or type / for commands", 231 | "isDefault": true, 232 | "locations": [ 233 | "editor" 234 | ], 235 | "defaultImplicitVariables": [ 236 | "_inlineChatContext", 237 | "_inlineChatDocument" 238 | ] 239 | } 240 | ], 241 | "viewsWelcome": [ 242 | { 243 | "view": "workbench.panel.chat.view.copilot", 244 | "contents": "Failed to activate Flexpilot. Please check the logs for more information.\n[View Logs](command:flexpilot.viewLogs)", 245 | "when": "flexpilot:isError" 246 | }, 247 | { 248 | "view": "workbench.panel.chat.view.copilot", 249 | "contents": "$(loading~spin) Please wait while Flexpilot is getting activated", 250 | "when": "!flexpilot:isError && !flexpilot:isLoaded" 251 | }, 252 | { 253 | "view": "workbench.panel.chat.view.copilot", 254 | "contents": "Sign in with GitHub to use Flexpilot, your AI pair programmer.\n[Sign In](command:flexpilot.github.signin)", 255 | "when": "!flexpilot:isError && flexpilot:isLoaded && !flexpilot:isLoggedIn" 256 | } 257 | ], 258 | "commands": [ 259 | { 260 | "command": "flexpilot.git.generateCommitMessage", 261 | "title": "Generate Commit Message", 262 | "icon": "$(sparkle)", 263 | "enablement": "false", 264 | "category": "Flexpilot" 265 | }, 266 | { 267 | "command": "flexpilot.github.signin", 268 | "title": "Sign In with GitHub" 269 | }, 270 | { 271 | "command": "flexpilot.configureModel", 272 | "title": "Configure the Language Model Provider", 273 | "category": "Flexpilot", 274 | "enablement": "flexpilot:isLoggedIn" 275 | }, 276 | { 277 | "command": "flexpilot.viewLogs", 278 | "category": "Flexpilot", 279 | "title": "View logs from Flexpilot output channel" 280 | }, 281 | { 282 | "command": "flexpilot.status.icon.menu", 283 | "enablement": "false", 284 | "title": "Status Icon Menu" 285 | } 286 | ], 287 | "icons": { 288 | "flexpilot-default": { 289 | "description": "Flexpilot Default Logo", 290 | "default": { 291 | "fontPath": "assets/fonts.woff", 292 | "fontCharacter": "\\e900" 293 | } 294 | }, 295 | "flexpilot-disabled": { 296 | "description": "Flexpilot Disabled Logo", 297 | "default": { 298 | "fontPath": "assets/fonts.woff", 299 | "fontCharacter": "\\e901" 300 | } 301 | } 302 | }, 303 | "iconFonts": [ 304 | { 305 | "id": "flexpilot-font", 306 | "src": [ 307 | { 308 | "path": "assets/fonts.woff", 309 | "format": "woff" 310 | } 311 | ] 312 | } 313 | ] 314 | }, 315 | "extensionKind": [ 316 | "ui" 317 | ], 318 | "dependencies": { 319 | "@ai-sdk/anthropic": "^0.0.51", 320 | "@ai-sdk/azure": "^0.0.45", 321 | "@ai-sdk/google": "^0.0.55", 322 | "@ai-sdk/mistral": "^0.0.42", 323 | "@ai-sdk/openai": "^0.0.66", 324 | "@flexpilot-ai/tokenizers": "^0.0.1", 325 | "@google/generative-ai": "^0.21.0", 326 | "@mistralai/mistralai": "^1.1.0", 327 | "@types/react": "^18.3.5", 328 | "@types/react-dom": "^18.3.0", 329 | "ai": "^3.4.29", 330 | "axios": "^1.7.2", 331 | "comment-json": "^4.2.4", 332 | "groq-sdk": "^0.5.0", 333 | "inversify": "^6.0.3", 334 | "markdown-it": "^14.1.0", 335 | "openai": "^4.67.3", 336 | "react": "^18.3.1", 337 | "react-dom": "^18.3.1", 338 | "reflect-metadata": "^0.2.2", 339 | "turndown": "^7.2.0", 340 | "turndown-plugin-gfm": "^1.0.2", 341 | "uuid": "^10.0.0", 342 | "zod": "^3.23.8" 343 | }, 344 | "lint-staged": { 345 | "*.{json,md}": [ 346 | "prettier --write" 347 | ], 348 | "*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}": [ 349 | "prettier --write", 350 | "eslint --fix" 351 | ] 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/commands/commit-message.ts: -------------------------------------------------------------------------------- 1 | import { generateText } from "ai"; 2 | import MarkdownIt from "markdown-it"; 3 | import * as vscode from "vscode"; 4 | import { GitExtension } from "../../types/git"; 5 | import { logger } from "../logger"; 6 | import { CommitMessagePrompt } from "../prompts/commands/commit-message"; 7 | import { ModelProviderManager } from "../providers"; 8 | import { storage } from "../storage"; 9 | 10 | /** 11 | * CommitMessageCommand class manages the commit message generation functionality. 12 | * It implements the Singleton pattern to ensure a single instance across the application. 13 | */ 14 | export class CommitMessageCommand { 15 | private static instance: CommitMessageCommand; 16 | 17 | /** 18 | * Private constructor to prevent direct instantiation. 19 | * Registers the command and initializes the disposable. 20 | */ 21 | private constructor(extensionContext = storage.getContext()) { 22 | // Register the command 23 | extensionContext.subscriptions.push( 24 | vscode.commands.registerCommand( 25 | "flexpilot.git.generateCommitMessage", 26 | this.handler.bind(this), 27 | ), 28 | ); 29 | logger.info("CommitMessageCommand instance created"); 30 | } 31 | 32 | /** 33 | * Gets the singleton instance of CommitMessageCommand. 34 | * @returns {CommitMessageCommand} The singleton instance. 35 | */ 36 | public static register() { 37 | if (!CommitMessageCommand.instance) { 38 | CommitMessageCommand.instance = new CommitMessageCommand(); 39 | logger.debug("New CommitMessageCommand instance created"); 40 | } 41 | } 42 | 43 | /** 44 | * Handles the commit message generation process. 45 | * @param {vscode.Uri} repositoryUri - The URI of the Git repository. 46 | * @param {unknown} _ISCMInputValueProviderContext - SCM input value provider context (unused). 47 | * @param {vscode.CancellationToken} token - Cancellation token for the operation. 48 | */ 49 | public async handler( 50 | repositoryUri: vscode.Uri, 51 | _ISCMInputValueProviderContext: unknown, 52 | token: vscode.CancellationToken, 53 | ): Promise { 54 | try { 55 | await vscode.window.withProgress( 56 | { 57 | location: vscode.ProgressLocation.SourceControl, 58 | }, 59 | async () => { 60 | logger.info("Starting commit message generation"); 61 | 62 | // Create an AbortController to handle cancellation 63 | const abortController = new AbortController(); 64 | token.onCancellationRequested(() => { 65 | abortController.abort(); 66 | logger.info("Commit message generation cancelled by user"); 67 | }); 68 | 69 | // Get the model provider for the commit message 70 | const provider = 71 | ModelProviderManager.getInstance().getProvider<"chat">( 72 | "Commit Message", 73 | ); 74 | if (!provider) { 75 | logger.notifyError("Model not configured for `Commit Message`"); 76 | return; 77 | } 78 | 79 | // Get the Git repository 80 | const gitExtension = 81 | vscode.extensions.getExtension("vscode.git"); 82 | if (!gitExtension) { 83 | throw new Error("Git extension is not installed or enabled."); 84 | } 85 | 86 | // Get the repository from the Git extension 87 | const repository = gitExtension.exports 88 | .getAPI(1) 89 | .getRepository(repositoryUri); 90 | if (!repository) { 91 | throw new Error(`Git Repository not found at ${repositoryUri}`); 92 | } 93 | 94 | // Prepare prompts for the chat 95 | const messages = await CommitMessagePrompt.build(repository); 96 | if (!messages) { 97 | logger.info("No prompt messages generated, skipping"); 98 | return; 99 | } 100 | 101 | // Generate the commit message 102 | const response = await generateText({ 103 | model: await provider.model(), 104 | messages: messages, 105 | abortSignal: abortController.signal, 106 | stopSequences: [], 107 | temperature: storage.workspace.get( 108 | "flexpilot.gitCommitMessage.temperature", 109 | ), 110 | }); 111 | 112 | // Log the model response 113 | logger.debug(`Model Response: ${response.text}`); 114 | 115 | // Parse the response for the commit message 116 | const parsedTokens = new MarkdownIt() 117 | .parse(response.text, {}) 118 | .filter((token) => token.type === "fence" && token.tag === "code"); 119 | if ( 120 | parsedTokens.length < 1 || 121 | parsedTokens[0].content.trim().length < 1 122 | ) { 123 | throw new Error("Model did not return a valid commit message"); 124 | } 125 | 126 | const commitMessage = parsedTokens[0].content.trim(); 127 | repository.inputBox.value = commitMessage; 128 | 129 | // Log the completion of the commit message generation 130 | logger.info("Commit message generation completed successfully"); 131 | }, 132 | ); 133 | } catch (error) { 134 | logger.error(error as Error); 135 | logger.notifyError("Error processing `Commit Message` request"); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/commands/github-sign-in.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { logger } from "../logger"; 3 | import { storage } from "../storage"; 4 | 5 | /** 6 | * GithubSignInCommand class manages the GitHub sign-in functionality. 7 | * It implements the Singleton pattern to ensure a single instance across the application. 8 | */ 9 | export class GithubSignInCommand { 10 | private static instance: GithubSignInCommand; 11 | 12 | /** 13 | * Private constructor to prevent direct instantiation. 14 | * Registers the command and initializes the disposable. 15 | */ 16 | private constructor(extensionContext = storage.getContext()) { 17 | // Register the command 18 | extensionContext.subscriptions.push( 19 | vscode.commands.registerCommand( 20 | "flexpilot.github.signin", 21 | this.handler.bind(this), 22 | ), 23 | ); 24 | logger.info("GithubSignInCommand instance created"); 25 | } 26 | 27 | /** 28 | * Gets the singleton instance of GithubSignInCommand. 29 | * @returns {GithubSignInCommand} The singleton instance. 30 | */ 31 | public static register() { 32 | if (!GithubSignInCommand.instance) { 33 | GithubSignInCommand.instance = new GithubSignInCommand(); 34 | logger.debug("New GithubSignInCommand instance created"); 35 | } 36 | } 37 | 38 | /** 39 | * Handles the GitHub sign-in process. 40 | * Prompts for GitHub star support if not previously set, then initiates the sign-in. 41 | */ 42 | public async handler(): Promise { 43 | try { 44 | logger.info("Handling `GithubSignInCommand`"); 45 | // Check if the user has already accepted GitHub support 46 | const githubSupportStatus = storage.get("github.support"); 47 | 48 | if (!githubSupportStatus) { 49 | const shouldSupport = await this.promptForGithubSupport(); 50 | logger.info( 51 | `User opted to ${ 52 | shouldSupport ? "support" : "not support" 53 | } the project with a GitHub star`, 54 | ); 55 | await storage.set("github.support", shouldSupport); 56 | } 57 | 58 | await vscode.authentication.getSession( 59 | "github", 60 | ["public_repo", "user:email"], 61 | { createIfNone: true }, 62 | ); 63 | 64 | // Set the context to indicate successful sign-in for walkthroughs 65 | await vscode.commands.executeCommand( 66 | "setContext", 67 | "flexpilot:walkthroughSignin", 68 | true, 69 | ); 70 | 71 | logger.notifyInfo("Successfully signed in with GitHub"); 72 | } catch (error) { 73 | logger.error(error as Error); 74 | logger.notifyError("Error in `Github Sign In` command"); 75 | } 76 | } 77 | 78 | /** 79 | * Prompts the user to support the project with a GitHub star. 80 | * @returns {Promise} True if the user agrees to support, false otherwise. 81 | */ 82 | private async promptForGithubSupport(): Promise { 83 | const selectedOption = await vscode.window.showInformationMessage( 84 | "Flexpilot: Support Us!", 85 | { 86 | modal: true, 87 | detail: 88 | "Help our open-source project stay alive. We'll auto-star on GitHub when you sign in. No extra steps!", 89 | }, 90 | "Proceed to Login", 91 | "No, I don't support", 92 | ); 93 | return !selectedOption || selectedOption === "Proceed to Login"; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/commands/status-icon-menu.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { events } from "../events"; 3 | import { logger } from "../logger"; 4 | import { statusIcon } from "../status-icon"; 5 | import { storage } from "../storage"; 6 | 7 | /** 8 | * Extends vscode.QuickPickItem with additional properties for custom functionality. 9 | */ 10 | interface ICustomQuickPickItem extends vscode.QuickPickItem { 11 | status?: string; 12 | handler?: () => Promise; 13 | } 14 | 15 | /** 16 | * StatusIconMenuCommand class manages the status icon menu functionality. 17 | * It implements the Singleton pattern to ensure a single instance across the application. 18 | */ 19 | export class StatusIconMenuCommand { 20 | private static instance: StatusIconMenuCommand; 21 | 22 | /** 23 | * Private constructor to prevent direct instantiation. 24 | * Registers the command and initializes the disposable. 25 | */ 26 | private constructor(extensionContext = storage.getContext()) { 27 | // Register the command 28 | extensionContext.subscriptions.push( 29 | vscode.commands.registerCommand( 30 | "flexpilot.status.icon.menu", 31 | this.handler.bind(this), 32 | ), 33 | ); 34 | logger.info("StatusIconMenuCommand instance created"); 35 | } 36 | 37 | /** 38 | * Gets the singleton instance of StatusIconMenuCommand. 39 | * @returns {StatusIconMenuCommand} The singleton instance. 40 | */ 41 | public static register() { 42 | if (!StatusIconMenuCommand.instance) { 43 | StatusIconMenuCommand.instance = new StatusIconMenuCommand(); 44 | logger.debug("New StatusIconMenuCommand instance created"); 45 | } 46 | } 47 | 48 | /** 49 | * Handles the status bar icon menu functionality. 50 | * Displays a quick pick menu with various options and executes the selected action. 51 | */ 52 | public async handler(): Promise { 53 | try { 54 | logger.info("Handling `StatusIconMenuCommand`"); 55 | const menuItems: ICustomQuickPickItem[] = this.createMenuItems(); 56 | const selectedMenu = await vscode.window.showQuickPick(menuItems, { 57 | placeHolder: "Select an option", 58 | ignoreFocusOut: true, 59 | title: "Flexpilot Completions Menu", 60 | }); 61 | 62 | if (selectedMenu?.handler) { 63 | await selectedMenu.handler(); 64 | } 65 | } catch (error) { 66 | logger.error(error as Error); 67 | logger.notifyError("Error in `Status Icon Menu` command"); 68 | } 69 | } 70 | 71 | /** 72 | * Creates the menu items for the quick pick menu. 73 | * @returns {ICustomQuickPickItem[]} An array of custom quick pick items. 74 | */ 75 | private createMenuItems(): ICustomQuickPickItem[] { 76 | let menuItems: ICustomQuickPickItem[] = []; 77 | 78 | // Add status item 79 | menuItems.push(this.createStatusMenuItem()); 80 | 81 | // Add separator 82 | menuItems.push({ label: "", kind: vscode.QuickPickItemKind.Separator }); 83 | 84 | // Add language-specific item if there's an active text editor 85 | const activeTextEditor = vscode.window.activeTextEditor; 86 | if (activeTextEditor) { 87 | menuItems.push(this.getLanguageSpecificMenuItem(activeTextEditor)); 88 | } 89 | 90 | // Add separator and general menu items 91 | menuItems.push({ label: "", kind: vscode.QuickPickItemKind.Separator }); 92 | 93 | // Add general menu items 94 | menuItems = menuItems.concat(this.createGeneralMenuItems()); 95 | 96 | return menuItems; 97 | } 98 | 99 | /** 100 | * Creates the status menu item based on the current configuration and state. 101 | * @returns {ICustomQuickPickItem} The status menu item. 102 | */ 103 | private createStatusMenuItem(): ICustomQuickPickItem { 104 | if (!storage.usage.get("Inline Completion")) { 105 | return { 106 | label: "$(flexpilot-default) Status: Inline Completion Not Configured", 107 | }; 108 | } 109 | return statusIcon.state === "disabled" 110 | ? { label: "$(flexpilot-default) Status: Disabled" } 111 | : { label: "$(flexpilot-default) Status: Ready" }; 112 | } 113 | 114 | /** 115 | * Creates general menu items for common actions. 116 | * @returns {ICustomQuickPickItem[]} An array of general menu items. 117 | */ 118 | private createGeneralMenuItems(): ICustomQuickPickItem[] { 119 | return [ 120 | { 121 | label: "$(keyboard) Edit Keyboard Shortcuts...", 122 | handler: async () => { 123 | vscode.commands.executeCommand( 124 | "workbench.action.openGlobalKeybindings", 125 | ); 126 | }, 127 | }, 128 | { 129 | label: "$(settings-gear) Edit Settings...", 130 | handler: async () => { 131 | vscode.commands.executeCommand( 132 | "workbench.action.openSettings", 133 | "flexpilot", 134 | ); 135 | }, 136 | }, 137 | { 138 | label: "$(chat-editor-label-icon) Open Flexpilot Chat", 139 | handler: async () => { 140 | vscode.commands.executeCommand( 141 | "workbench.panel.chat.view.copilot.focus", 142 | ); 143 | }, 144 | }, 145 | { 146 | label: "$(remote-explorer-documentation) View Flexpilot Docs...", 147 | handler: async () => { 148 | vscode.env.openExternal( 149 | vscode.Uri.parse("https://docs.flexpilot.ai"), 150 | ); 151 | }, 152 | }, 153 | ]; 154 | } 155 | 156 | /** 157 | * Creates a language-specific menu item for enabling or disabling completions. 158 | * @param {vscode.TextEditor} activeTextEditor The currently active text editor. 159 | * @returns {ICustomQuickPickItem} A menu item for toggling completions for the current language. 160 | */ 161 | private getLanguageSpecificMenuItem( 162 | activeTextEditor: vscode.TextEditor, 163 | ): ICustomQuickPickItem { 164 | const config = storage.get("completions.disabled.languages") || []; 165 | const languageId = activeTextEditor.document.languageId; 166 | const isDisabled = config.includes(languageId); 167 | 168 | return { 169 | label: `\`${ 170 | isDisabled ? "Enable" : "Disable" 171 | }\` Completions for \`${languageId}\``, 172 | handler: async () => { 173 | if (isDisabled) { 174 | config.splice(config.indexOf(languageId), 1); 175 | } else { 176 | config.push(languageId); 177 | } 178 | await storage.set("completions.disabled.languages", config); 179 | events.fire({ 180 | name: "inlineCompletionProviderUpdated", 181 | payload: { updatedAt: Date.now() }, 182 | }); 183 | }, 184 | }; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the types of providers available in the application. 3 | * These types are used to categorize different functionalities. 4 | */ 5 | export const PROVIDER_TYPES = ["chat", "completion"] as const; 6 | 7 | /** 8 | * Defines the models that are allowed to be used for completions. 9 | * Each model has a regex pattern to match the model name, a context window size, 10 | * a type, and a tokenizer URL. 11 | */ 12 | export const ALLOWED_COMPLETION_MODELS = [ 13 | { 14 | regex: "^gpt-3.5-turbo-.*instruct", 15 | contextWindow: 4000, 16 | tokenizerUrl: 17 | "https://cdn.jsdelivr.net/gh/flexpilot-ai/vscode-extension/tokenizers/cl100k_base.json", 18 | }, 19 | { 20 | regex: "^codestral-(?!.*mamba)", 21 | contextWindow: 31500, 22 | tokenizerUrl: 23 | "https://cdn.jsdelivr.net/gh/flexpilot-ai/vscode-extension/tokenizers/codestral-v0.1.json", 24 | }, 25 | { 26 | regex: "^gpt-35-turbo-.*instruct", 27 | contextWindow: 4000, 28 | tokenizerUrl: 29 | "https://cdn.jsdelivr.net/gh/flexpilot-ai/vscode-extension/tokenizers/cl100k_base.json", 30 | }, 31 | ]; 32 | 33 | /** 34 | * Defines the various locations or contexts where AI assistance is provided. 35 | * Each location has a name, description, and associated provider type. 36 | */ 37 | export const LOCATIONS = [ 38 | { 39 | name: "Chat Suggestions", 40 | description: "Suggestions that appear in the panel chat", 41 | type: "chat", 42 | }, 43 | { 44 | name: "Chat Title", 45 | description: "Dynamically generated title for the chat", 46 | type: "chat", 47 | }, 48 | { 49 | name: "Inline Chat", 50 | description: "Chat used inline inside an active file", 51 | type: "chat", 52 | }, 53 | { 54 | name: "Panel Chat", 55 | description: "Chat that appears on the panel on the side", 56 | type: "chat", 57 | }, 58 | { 59 | name: "Commit Message", 60 | description: "Used to generate commit messages for source control", 61 | type: "chat", 62 | }, 63 | { 64 | name: "Inline Completion", 65 | description: "Completion suggestions in the active file", 66 | type: "completion", 67 | }, 68 | ] as const; 69 | 70 | // Note: The 'as const' assertion ensures that the array is read-only 71 | // and its contents are treated as literal types. 72 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { logger } from "./logger"; 3 | import { storage } from "./storage"; 4 | 5 | /** 6 | * IEventPayload represents the payload of an event. 7 | */ 8 | interface IEventPayload { 9 | name: "modelProvidersUpdated" | "inlineCompletionProviderUpdated"; 10 | payload: Record; 11 | } 12 | 13 | /** 14 | * EVENT_KEY_PREFIX is the prefix used for event keys in the storage. 15 | */ 16 | const EVENT_KEY_PREFIX = "window.events."; 17 | 18 | /** 19 | * EventsSingleton is a singleton class that allows for the firing and listening of events. 20 | * For local events, consider using the built-in VS Code event system. 21 | * This class is used to fire events across multiple open VS Code windows. 22 | */ 23 | export class EventsSingleton { 24 | private static instance: EventsSingleton; 25 | private readonly eventEmitter = new vscode.EventEmitter(); 26 | 27 | /** 28 | * Creates a new instance of EventsSingleton. 29 | */ 30 | private constructor( 31 | private readonly extensionContext = storage.getContext(), 32 | ) { 33 | // Listen for changes in the secrets storage to fire events 34 | extensionContext.subscriptions.push( 35 | extensionContext.secrets.onDidChange(async (event) => { 36 | if (event.key.startsWith(EVENT_KEY_PREFIX)) { 37 | logger.debug(`Event received: ${event.key}`); 38 | const payload = await extensionContext.secrets.get(event.key); 39 | if (payload) { 40 | logger.debug(`Event payload: ${JSON.stringify(payload)}`); 41 | this.eventEmitter.fire(JSON.parse(payload)); 42 | } else { 43 | logger.debug(`Event payload empty, skipped firing`); 44 | } 45 | } 46 | }), 47 | ); 48 | } 49 | 50 | /** 51 | * Returns the singleton instance of EventsSingleton. 52 | * @returns {EventsSingleton} The singleton instance. 53 | */ 54 | public static getInstance(): EventsSingleton { 55 | if (!EventsSingleton.instance) { 56 | EventsSingleton.instance = new EventsSingleton(); 57 | logger.info("EventsSingleton instance created"); 58 | } 59 | return EventsSingleton.instance; 60 | } 61 | 62 | /** 63 | * Fires an event with the given payload. 64 | * @param {IEventPayload} event - The event to fire. 65 | */ 66 | public async fire(event: IEventPayload) { 67 | logger.debug( 68 | `Firing event: ${event.name} with payload: ${JSON.stringify(event.payload)}`, 69 | ); 70 | await this.extensionContext.secrets.store( 71 | `${EVENT_KEY_PREFIX}${event.name}`, 72 | JSON.stringify(event), 73 | ); 74 | } 75 | 76 | /** 77 | * Event that is fired when an event is emitted. 78 | */ 79 | public onFire = this.eventEmitter.event; 80 | } 81 | 82 | // Export a singleton instance of the events 83 | export const events = EventsSingleton.getInstance(); 84 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { logger } from "./logger"; 3 | import { storage } from "./storage"; 4 | 5 | /** 6 | * Activates the extension. 7 | */ 8 | export async function activate(context: vscode.ExtensionContext) { 9 | // log system information 10 | logger.info("VS Code Version:", vscode.version); 11 | logger.info("Node Version:", process.version); 12 | logger.info("Platform:", process.platform); 13 | logger.info("CPU Architecture:", process.arch); 14 | logger.info("Extension ID:", context.extension.id); 15 | logger.info("Extension Version:", context.extension.packageJSON.version); 16 | logger.info("Extension Path:", context.extension.extensionPath); 17 | 18 | // log workspace information 19 | (vscode.workspace.workspaceFolders || []).forEach((folder) => { 20 | logger.info("Workspace Name:", folder.name); 21 | logger.info("Workspace URI:", folder.uri.toString()); 22 | }); 23 | logger.info("Environment Variables:", process.env); 24 | 25 | // Initialize the storage manager 26 | storage.setContext(context); 27 | 28 | // Register the logger with the context 29 | context.subscriptions.push(logger); 30 | 31 | // Show a warning message to users 32 | vscode.window 33 | .showWarningMessage( 34 | "Flexpilot VS Code extension is no longer actively maintained; switch to Flexpilot IDE, a VS Code fork with better performance, multi-file editing, a web client, and more features. We will still try to address issues and pull requests, but we won't be adding new features to this extension.", 35 | "Download Flexpilot IDE", 36 | ) 37 | .then((selection) => { 38 | if (selection === "Download Flexpilot IDE") { 39 | vscode.env.openExternal( 40 | vscode.Uri.parse( 41 | "https://flexpilot.ai/docs/getting-started#downloading-the-ide", 42 | ), 43 | ); 44 | } 45 | }); 46 | 47 | // Show message to user to hide the default copilot signup page 48 | if (!context.globalState.get("hideWarningMessage")) { 49 | vscode.window 50 | .showWarningMessage( 51 | "If you see a Copilot signup page in the chat panel by default, please click the 'Hide Copilot' button to dismiss it.", 52 | "Hide Default Copilot Sign Up", 53 | "Don't Show Again", 54 | ) 55 | .then((selection) => { 56 | if (selection === "Don't Show Again") { 57 | context.globalState.update("hideWarningMessage", true); 58 | } else if (selection === "Hide Default Copilot Sign Up") { 59 | vscode.commands.executeCommand("workbench.action.chat.hideSetup"); 60 | } 61 | }); 62 | } 63 | 64 | // lazy load the extension 65 | await (await import("./lazy-load.js")).activate(); 66 | } 67 | -------------------------------------------------------------------------------- /src/inline-chat.ts: -------------------------------------------------------------------------------- 1 | import { streamText } from "ai"; 2 | import * as vscode from "vscode"; 3 | import { IChatResult } from "./interfaces"; 4 | import { logger } from "./logger"; 5 | import { InlineChatPrompt } from "./prompts/inline-chat"; 6 | import { ModelProviderManager } from "./providers"; 7 | import { storage } from "./storage"; 8 | import { getEol } from "./utilities"; 9 | 10 | /** 11 | * InlineChatParticipant class provides functionality for the inline chat feature in Flexpilot. 12 | * It implements the Singleton pattern to ensure a single instance across the application. 13 | */ 14 | class InlineChatParticipant { 15 | private static instance: InlineChatParticipant | null = null; 16 | private readonly chatParticipant: vscode.ChatParticipant; 17 | private readonly githubSession: vscode.AuthenticationSession; 18 | 19 | /** 20 | * Private constructor to prevent direct instantiation. 21 | * Initializes the chat participant with necessary providers and configurations. 22 | */ 23 | private constructor() { 24 | // Create the chat participant 25 | this.chatParticipant = vscode.chat.createChatParticipant( 26 | "flexpilot.editor.default", 27 | this.handleChatRequest.bind(this), 28 | ); 29 | 30 | // Get the GitHub session 31 | this.githubSession = storage.session.get(); 32 | 33 | // Set up requester information 34 | this.chatParticipant.requester = { 35 | name: this.githubSession.account.label, 36 | icon: vscode.Uri.parse( 37 | `https://avatars.githubusercontent.com/u/${this.githubSession.account.id}`, 38 | ), 39 | }; 40 | 41 | // Set chat participant icon 42 | this.chatParticipant.iconPath = new vscode.ThemeIcon("flexpilot-default"); 43 | } 44 | 45 | /** 46 | * Disposes the inline chat participant instance. 47 | */ 48 | public static dispose(): void { 49 | if (InlineChatParticipant.instance) { 50 | InlineChatParticipant.instance.chatParticipant.dispose(); 51 | InlineChatParticipant.instance = null; 52 | } 53 | logger.info("Inline chat participant disposed successfully"); 54 | } 55 | 56 | /** 57 | * Registers the inline chat participant instance. 58 | */ 59 | public static register() { 60 | if (!InlineChatParticipant.instance) { 61 | InlineChatParticipant.instance = new InlineChatParticipant(); 62 | logger.debug("Inline chat participant registered successfully"); 63 | } 64 | } 65 | 66 | /** 67 | * Handles the chat request and generates a response. 68 | * @param {vscode.ChatRequest} request - The chat request. 69 | * @param {vscode.ChatContext} context - The chat context. 70 | * @param {vscode.ChatResponseStream} response - The response stream. 71 | * @param {vscode.CancellationToken} token - The cancellation token. 72 | * @returns {Promise} The chat result. 73 | */ 74 | private async handleChatRequest( 75 | request: vscode.ChatRequest, 76 | context: vscode.ChatContext, 77 | response: vscode.ChatResponseStream, 78 | token: vscode.CancellationToken, 79 | ): Promise { 80 | const abortController = new AbortController(); 81 | token.onCancellationRequested(() => { 82 | abortController.abort(); 83 | }); 84 | 85 | try { 86 | // Get the chat provider 87 | const provider = 88 | ModelProviderManager.getInstance().getProvider<"chat">("Inline Chat"); 89 | if (!provider) { 90 | response.markdown("Click below button to configure model"); 91 | response.button({ 92 | command: "flexpilot.configureModel", 93 | title: "Configure Model", 94 | }); 95 | return { 96 | metadata: { 97 | response: "Unable to process request", 98 | request: request.prompt, 99 | }, 100 | errorDetails: { 101 | message: `Model not configured for \`Inline Chat\``, 102 | }, 103 | }; 104 | } 105 | 106 | // Prepare messages for the chat 107 | const prompt = new InlineChatPrompt(response, context, request); 108 | const messages = await prompt.build(); 109 | 110 | // Get the document and selection 111 | const { document, selection } = prompt.editor; 112 | const codeBoundary = prompt.getCodeBoundary(); 113 | 114 | // Generate the chat response 115 | const stream = await streamText({ 116 | model: await provider.model(), 117 | messages: messages, 118 | abortSignal: abortController.signal, 119 | stopSequences: [codeBoundary.end], 120 | temperature: storage.workspace.get( 121 | "flexpilot.inlineChat.temperature", 122 | ), 123 | }); 124 | 125 | let lastPushedIndex = 0; 126 | let final = ""; 127 | 128 | for await (const textPart of stream.fullStream) { 129 | // if part is not of text-delta skip 130 | if (textPart.type !== "text-delta") continue; 131 | 132 | // append the text part to the final response 133 | final = final.concat(textPart.textDelta); 134 | const lines = final.split("\n"); 135 | 136 | // get index of start and end boundary 137 | const trimmed = lines.map((line) => line.trim()); 138 | const startIdx = trimmed.indexOf(codeBoundary.start); 139 | let endIdx = trimmed.length; 140 | if (trimmed.includes(codeBoundary.end)) { 141 | endIdx = trimmed.indexOf(codeBoundary.end); 142 | } 143 | 144 | // continue if the start boundary is not found 145 | if (startIdx < 0) { 146 | continue; 147 | } 148 | 149 | // get the filtered lines between boundary 150 | const filtered = lines.slice(startIdx + 1, endIdx + 1); 151 | 152 | // push the filtered lines to the response 153 | for (let i = lastPushedIndex; i < filtered.length - 1; i++) { 154 | if (i === 0) { 155 | // replace the selection with the first line 156 | response.push( 157 | new vscode.ChatResponseTextEditPart(document.uri, { 158 | range: selection, 159 | newText: filtered[i] + getEol(document), 160 | }), 161 | ); 162 | } else { 163 | // insert the rest of the lines after the first line 164 | response.push( 165 | new vscode.ChatResponseTextEditPart(document.uri, { 166 | range: new vscode.Range( 167 | selection.start.translate(i), 168 | selection.start.translate(i), 169 | ), 170 | newText: filtered[i] + getEol(document), 171 | }), 172 | ); 173 | } 174 | lastPushedIndex = i + 1; 175 | } 176 | 177 | // break if the end boundary is found 178 | if (trimmed.includes(codeBoundary.end)) { 179 | break; 180 | } 181 | } 182 | 183 | // Check if token usage is enabled and show usage 184 | if (storage.workspace.get("flexpilot.inlineChat.showTokenUsage")) { 185 | const usage = await stream.usage; 186 | if (usage.completionTokens && usage.promptTokens) { 187 | response.warning( 188 | `Prompt Tokens: ${usage.promptTokens}, Completion Tokens: ${usage.completionTokens}`, 189 | ); 190 | } 191 | } 192 | 193 | // Log the model response 194 | logger.debug(`Model Response: ${await stream.text}`); 195 | return { metadata: { response: final, request: request.prompt } }; 196 | } catch (error) { 197 | logger.error(error as Error); 198 | logger.notifyError("Error processing `Inline Chat` request"); 199 | throw error; 200 | } 201 | } 202 | } 203 | 204 | // Export the InlineChatParticipant instance 205 | export default InlineChatParticipant; 206 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { LanguageModelV1 } from "ai"; 2 | import * as vscode from "vscode"; 3 | import packageJson from "../package.json"; 4 | import { LOCATIONS, PROVIDER_TYPES } from "./constants"; 5 | 6 | /** 7 | * Type for package.json. 8 | */ 9 | export type IPackageJson = typeof packageJson & { 10 | contributes?: { 11 | [key: string]: unknown; 12 | menus: { 13 | [key: string]: unknown; 14 | }; 15 | }; 16 | [key: string]: unknown; 17 | }; 18 | 19 | /** 20 | * Interface for completion model invoke options. 21 | */ 22 | export interface ICompletionModelInvokeOptions { 23 | maxTokens: number; 24 | stop: string[]; 25 | temperature: number; 26 | signal: AbortSignal; 27 | messages: { prefix: string; suffix: string }; 28 | } 29 | 30 | /** 31 | * Interface extending vscode.ChatResult with additional metadata. 32 | */ 33 | export interface IChatResult extends vscode.ChatResult { 34 | metadata: { response: string; request: string }; 35 | } 36 | 37 | /** 38 | * Type for location names based on model type. 39 | */ 40 | export type ILocationName = Extract< 41 | (typeof LOCATIONS)[number], 42 | { type: T } 43 | >["name"]; 44 | 45 | /** 46 | * Type for model types. 47 | */ 48 | export type IModelType = (typeof PROVIDER_TYPES)[number]; 49 | 50 | /** 51 | * Interface for usage preferences. 52 | */ 53 | export interface IUsagePreference { 54 | providerId: string; 55 | nickname: string; 56 | locationName: ILocationName; 57 | } 58 | 59 | /** 60 | * Abstract base class for model providers. 61 | */ 62 | abstract class IModelProviderBase { 63 | static readonly providerName: string; 64 | static readonly providerId: string; 65 | static readonly providerType: IModelType; 66 | 67 | constructor(public nickname: string) {} 68 | 69 | abstract config: IModelConfig; 70 | 71 | // Use this method only for async initialization, else use constructor 72 | async initialize(): Promise { 73 | return Promise.resolve(); 74 | } 75 | 76 | static configure(nickname: string): Promise { 77 | throw new Error(`Method not implemented for ${nickname}`); 78 | } 79 | } 80 | 81 | /** 82 | * Abstract class for chat model providers. 83 | */ 84 | export abstract class IChatModelProvider extends IModelProviderBase { 85 | static readonly providerType: "chat"; 86 | 87 | abstract model(): Promise; 88 | } 89 | 90 | /** 91 | * Abstract class for completion model providers. 92 | */ 93 | export abstract class ICompletionModelProvider extends IModelProviderBase { 94 | static readonly providerType: "completion"; 95 | 96 | abstract encode: (text: string) => Promise; 97 | 98 | abstract decode: (tokens: number[]) => Promise; 99 | 100 | abstract invoke(options: ICompletionModelInvokeOptions): Promise; 101 | } 102 | 103 | /** 104 | * Interface for model configuration. 105 | */ 106 | export interface IModelConfig { 107 | nickname: string; 108 | model: string; 109 | providerId: string; 110 | } 111 | 112 | /** 113 | * Interface for chat model invoke options. 114 | */ 115 | export interface ICompletionModelConfig extends IModelConfig { 116 | contextWindow: number; 117 | } 118 | 119 | /** 120 | * Interface representing the configuration for a language. 121 | */ 122 | export interface ILanguageConfig { 123 | comment: { 124 | start: string; 125 | end: string; 126 | }; 127 | markdown: string; 128 | } 129 | -------------------------------------------------------------------------------- /src/lazy-load.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { CommitMessageCommand } from "./commands/commit-message"; 3 | import { ConfigureModelCommand } from "./commands/configure-model"; 4 | import { GithubSignInCommand } from "./commands/github-sign-in"; 5 | import { StatusIconMenuCommand } from "./commands/status-icon-menu"; 6 | import { events } from "./events"; 7 | import { logger } from "./logger"; 8 | import { ProxyModelProvider } from "./models"; 9 | import { SessionManager } from "./session"; 10 | import { updateRuntimeArguments } from "./startup"; 11 | import { setContext } from "./utilities"; 12 | import { VariablesManager } from "./variables"; 13 | 14 | /** 15 | * Activates the extension. 16 | */ 17 | export const activate = async () => { 18 | // set initial values to context variables 19 | setContext("isLoaded", false); 20 | setContext("isError", false); 21 | setContext("isLoggedIn", false); 22 | try { 23 | // Update the runtime arguments 24 | await updateRuntimeArguments(); 25 | 26 | // Register the variables manager 27 | VariablesManager.register(); 28 | 29 | // Register the proxy model 30 | ProxyModelProvider.register(); 31 | 32 | // Register the commands 33 | StatusIconMenuCommand.register(); 34 | CommitMessageCommand.register(); 35 | GithubSignInCommand.register(); 36 | ConfigureModelCommand.register(); 37 | 38 | // Handle the session change 39 | SessionManager.register(); 40 | 41 | // Update the status bar icon 42 | events.fire({ 43 | name: "inlineCompletionProviderUpdated", 44 | payload: { updatedAt: Date.now() }, 45 | }); 46 | 47 | // Set the loaded context 48 | setContext("isLoaded", true); 49 | 50 | // Open chat view on startup 51 | vscode.commands.executeCommand("workbench.action.chat.open"); 52 | } catch (error) { 53 | // Set the error context 54 | setContext("isError", true); 55 | 56 | // Log the error to the output channel 57 | logger.error(error as Error); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from "axios"; 2 | import * as vscode from "vscode"; 3 | 4 | /** 5 | * LoggerSingleton class provides a centralized logging mechanism for the Flexpilot VS Code extension. 6 | * It implements the Singleton pattern to ensure a single instance of the logger across the application. 7 | */ 8 | export class LoggerSingleton extends vscode.Disposable { 9 | private readonly disposable: vscode.Disposable; 10 | private static instance: LoggerSingleton; 11 | private readonly outputChannel: vscode.LogOutputChannel; 12 | 13 | /** 14 | * Creates a new instance of LoggerSingleton. 15 | */ 16 | private constructor() { 17 | // Call the parent constructor 18 | super(() => { 19 | // Dispose the output channel 20 | this.outputChannel.dispose(); 21 | // Dispose the command registration 22 | this.disposable.dispose(); 23 | }); 24 | 25 | // Create the output channel 26 | this.outputChannel = vscode.window.createOutputChannel("Flexpilot", { 27 | log: true, 28 | }); 29 | 30 | // Register the command to view logs 31 | this.disposable = vscode.commands.registerCommand( 32 | "flexpilot.viewLogs", 33 | () => this.outputChannel.show(), 34 | ); 35 | } 36 | 37 | /** 38 | * Shows the output channel. 39 | */ 40 | public showOutputChannel(): void { 41 | this.outputChannel.show(); 42 | } 43 | 44 | /** 45 | * Returns the singleton instance of LoggerSingleton. 46 | * @returns {LoggerSingleton} The singleton instance. 47 | */ 48 | public static getInstance(): LoggerSingleton { 49 | if (!LoggerSingleton.instance) { 50 | LoggerSingleton.instance = new LoggerSingleton(); 51 | } 52 | return LoggerSingleton.instance; 53 | } 54 | 55 | /** 56 | * Logs an informational message to the output channel. 57 | */ 58 | public info(message: string, ...args: unknown[]): void { 59 | this.outputChannel.info(message, ...args); 60 | } 61 | 62 | /** 63 | * Logs an informational message and shows a notification. 64 | */ 65 | public notifyInfo(message: string, ...args: unknown[]): void { 66 | this.outputChannel.info(message, ...args); 67 | 68 | // Show information notification 69 | vscode.window 70 | .showInformationMessage(message, "Open Docs") 71 | .then(async (selection) => { 72 | if (selection === "Open Docs") { 73 | vscode.env.openExternal( 74 | vscode.Uri.parse("https://docs.flexpilot.ai/"), 75 | ); 76 | } 77 | }); 78 | } 79 | 80 | /** 81 | * Logs a warning message to the output channel. 82 | */ 83 | public warn(message: string, ...args: unknown[]): void { 84 | this.outputChannel.warn(message, ...args); 85 | } 86 | 87 | /** 88 | * Logs an warning message and shows a notification. 89 | */ 90 | public notifyWarn(message: string, ...args: unknown[]): void { 91 | this.outputChannel.warn(message, ...args); 92 | 93 | // Show warning notification 94 | vscode.window 95 | .showWarningMessage(message, "View Details") 96 | .then((selection) => { 97 | if (selection === "View Details") { 98 | this.outputChannel.show(); 99 | } 100 | }); 101 | } 102 | 103 | /** 104 | * Logs a debug message to the output channel. 105 | */ 106 | public debug(message: string, ...args: unknown[]): void { 107 | this.outputChannel.debug(message, ...args); 108 | } 109 | 110 | /** 111 | * Logs an error message to the output channel. 112 | */ 113 | public error(error: string | Error, ...args: unknown[]): void { 114 | this.outputChannel.error(error, ...args); 115 | } 116 | 117 | /** 118 | * Logs an error message and shows a notification. 119 | */ 120 | public notifyError(message: string): void { 121 | this.outputChannel.error(message); 122 | 123 | // Show error notification 124 | vscode.window 125 | .showErrorMessage(message, "View Details") 126 | .then((selection) => { 127 | if (selection === "View Details") { 128 | this.outputChannel.show(); 129 | } 130 | }); 131 | } 132 | } 133 | 134 | // Export a singleton instance of the logger 135 | export const logger = LoggerSingleton.getInstance(); 136 | 137 | // Axios request interceptor 138 | axios.interceptors.request.use( 139 | (config) => { 140 | // Log the request details and return the config 141 | logger.debug("Axios Request:", { 142 | headers: config.headers, 143 | url: config.url, 144 | method: config.method, 145 | data: Buffer.isBuffer(config.data) ? "" : config.data, 146 | baseURL: config.baseURL, 147 | }); 148 | return config; 149 | }, 150 | (error: AxiosError) => { 151 | // Log the request details and reject the promise 152 | logger.error("Axios Request:", error); 153 | return Promise.reject(error); 154 | }, 155 | ); 156 | 157 | // Axios response interceptor 158 | axios.interceptors.response.use( 159 | (response) => { 160 | // Log the response details and return the response 161 | logger.debug("Axios Response:", { 162 | status: response.status, 163 | data: Buffer.isBuffer(response.data) ? "" : response.data, 164 | headers: response.headers, 165 | }); 166 | return response; 167 | }, 168 | (error: AxiosError) => { 169 | if (error.response) { 170 | // The request was made and the server responded with a status code 171 | logger.error("Axios Error:", { 172 | status: error.response.status, 173 | data: Buffer.isBuffer(error.response.data) 174 | ? "" 175 | : error.response.data, 176 | headers: error.response.headers, 177 | }); 178 | } else if (error.request) { 179 | // No response received 180 | logger.error("Axios Error (No Response):", error.request); 181 | } else { 182 | // Something happened in setting up the request 183 | logger.error("Axios Error:", error.message); 184 | } 185 | return Promise.reject(error); 186 | }, 187 | ); 188 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * ProxyModelProvider class provides functionality for the proxy model. 5 | * It implements the Singleton pattern to ensure a single instance across the application. 6 | */ 7 | export class ProxyModelProvider { 8 | private static instance: ProxyModelProvider | null = null; 9 | 10 | /** 11 | * Private constructor to prevent direct instantiation. 12 | * Initializes the proxy model provider. 13 | */ 14 | private constructor() { 15 | // Register the proxy model 16 | vscode.lm.registerChatModelProvider( 17 | "proxy", 18 | { 19 | provideLanguageModelResponse() { 20 | throw new Error("Method not implemented."); 21 | }, 22 | provideTokenCount() { 23 | throw new Error("Method not implemented."); 24 | }, 25 | }, 26 | { 27 | maxInputTokens: 10000, 28 | family: "proxy", 29 | name: "proxy", 30 | isDefault: true, 31 | vendor: "flexpilot", 32 | version: "1.0.0", 33 | maxOutputTokens: 10000, 34 | }, 35 | ); 36 | } 37 | 38 | /** 39 | * Returns the singleton instance of ProxyModelProvider. 40 | * @returns {ProxyModelProvider} The singleton instance. 41 | */ 42 | public static register(): ProxyModelProvider { 43 | if (!ProxyModelProvider.instance) { 44 | ProxyModelProvider.instance = new ProxyModelProvider(); 45 | } 46 | return ProxyModelProvider.instance; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/prompts/commands/commit-message.tsx: -------------------------------------------------------------------------------- 1 | import { CoreMessage } from "ai"; 2 | import * as vscode from "vscode"; 3 | import { Repository } from "../../../types/git"; 4 | import { logger } from "../../logger"; 5 | import { Code, jsxToCoreMessage, Message } from "../jsx-utilities"; 6 | 7 | /** 8 | * CommitMessagePrompt class handles the generation of prompts for commit message suggestions. 9 | * It manages the creation of system, code diff, repository commits, and user prompts. 10 | */ 11 | export class CommitMessagePrompt { 12 | /** 13 | * Generates the system prompt for commit message generation. 14 | * @returns {CoreMessage} The system prompt as a CoreMessage. 15 | */ 16 | private static getSystemPrompt(): CoreMessage { 17 | logger.debug("Generating system prompt for commit message"); 18 | return jsxToCoreMessage( 19 | 20 |

21 | You are an AI programming assistant, helping a software developer to 22 | come up with the best git commit message for their code changes. You 23 | excel in interpreting the purpose behind code changes to craft 24 | succinct, clear commit messages that adhere to the repository's 25 | guidelines. 26 |

27 |

Examples of commit messages:

28 |
    29 |
  • 30 | 31 | feat: improve page load with lazy loading for images 32 | 33 |
  • 34 |
  • 35 | 36 | Fix bug preventing submitting the signup form 37 | 38 |
  • 39 |
  • 40 | 41 | chore: update npm dependency to latest stable version 42 | 43 |
  • 44 |
  • 45 | 46 | Update landing page banner color per client request 47 | 48 |
  • 49 |
50 |
    51 |
  • 52 | First, think step-by-step, Analyze the CODE CHANGES thoroughly to 53 | understand what's been modified. 54 |
  • 55 |
  • 56 | Identify the purpose of the changes to answer the "why" for the 57 | commit messages, also considering the optionally provided RECENT 58 | USER COMMITS. 59 |
  • 60 |
  • 61 | Review the provided RECENT REPOSITORY COMMITS to identify 62 | established commit message conventions. Focus on the format and 63 | style, ignoring commit-specific details like refs, tags, and 64 | authors. 65 |
  • 66 |
  • 67 | Generate a thoughtful and succinct commit message for the given CODE 68 | CHANGES. It MUST follow the established writing conventions. 69 |
  • 70 |
  • 71 | Remove any meta information like issue references, tags, or author 72 | names from the commit message. The developer will add them. 73 |
  • 74 |
  • 75 | Now only show your message, wrapped with a single markdown ```text 76 | codeblock! Do not provide any explanations or details. 77 |
  • 78 |
79 |
, 80 | ); 81 | } 82 | 83 | /** 84 | * Generates the code diff prompt. 85 | * @param {string} diff - The git diff string. 86 | * @returns {CoreMessage} The code diff prompt as a CoreMessage. 87 | */ 88 | private static getCodeDiffPrompt(diff: string): CoreMessage { 89 | logger.debug("Generating code diff prompt"); 90 | return jsxToCoreMessage( 91 | 92 |

CODE CHANGES:

93 | {diff} 94 |
, 95 | ); 96 | } 97 | 98 | /** 99 | * Generates the repository commits prompt. 100 | * @param {Repository} repository - The git repository. 101 | * @returns {Promise} A promise that resolves to the repository commits prompt as a CoreMessage, or undefined if no commits are found. 102 | */ 103 | private static async getRepositoryCommitsPrompt( 104 | repository: Repository, 105 | ): Promise { 106 | logger.debug("Generating repository commits prompt"); 107 | const recentCommits = await repository.log({ maxEntries: 10 }); 108 | if (!recentCommits.length) { 109 | logger.debug("No recent commits found"); 110 | return undefined; 111 | } 112 | return jsxToCoreMessage( 113 | 114 |

RECENT REPOSITORY COMMITS:

115 |
    116 | {recentCommits.map((log) => ( 117 |
  • 118 | {log.message} 119 |
  • 120 | ))} 121 |
122 |
, 123 | ); 124 | } 125 | 126 | /** 127 | * Generates the author commits prompt. 128 | * @param {Repository} repository - The git repository. 129 | * @returns {Promise} A promise that resolves to the author commits prompt as a CoreMessage, or undefined if no commits are found. 130 | */ 131 | private static async getAuthorCommitsPrompt( 132 | repository: Repository, 133 | ): Promise { 134 | logger.debug("Generating author commits prompt"); 135 | const authorName = 136 | (await repository.getConfig("user.name")) ?? 137 | (await repository.getGlobalConfig("user.name")); 138 | const authorCommits = await repository.log({ 139 | maxEntries: 10, 140 | author: authorName, 141 | }); 142 | if (!authorCommits.length) { 143 | logger.debug("No author commits found"); 144 | return undefined; 145 | } 146 | return jsxToCoreMessage( 147 | 148 |

RECENT AUTHOR COMMITS:

149 |
    150 | {authorCommits.map((log) => ( 151 |
  • 152 | {log.message} 153 |
  • 154 | ))} 155 |
156 |
, 157 | ); 158 | } 159 | 160 | /** 161 | * Generates the user prompt. 162 | * @returns {CoreMessage} The user prompt as a CoreMessage. 163 | */ 164 | private static getUserPrompt(): CoreMessage { 165 | logger.debug("Generating user prompt"); 166 | return jsxToCoreMessage( 167 | 168 |

169 | Remember to ONLY return a single markdown ```text code block with the 170 | suggested commit message. NO OTHER PROSE! If you write more than the 171 | commit message, your commit message gets lost. 172 |

173 |

Example:

174 | commit message goes here 175 |
, 176 | ); 177 | } 178 | 179 | /** 180 | * Handles the case when no diff is found. 181 | * @param {Repository} repository - The git repository. 182 | */ 183 | private static handleNoDiff(repository: Repository): void { 184 | logger.warn("No staged changes found to commit"); 185 | vscode.window 186 | .showErrorMessage( 187 | "No staged changes found to commit. Please stage changes and try again.", 188 | "Stage All Changes", 189 | ) 190 | .then((selection) => { 191 | if (selection === "Stage All Changes") { 192 | logger.info("User chose to stage all changes"); 193 | vscode.commands.executeCommand("git.stageAll", repository); 194 | } 195 | }); 196 | } 197 | 198 | /** 199 | * Builds the complete set of prompts for commit message generation. 200 | * @param {Repository} repository - The git repository. 201 | * @returns {Promise} A promise that resolves to an array of CoreMessages or undefined if no diff is found. 202 | */ 203 | public static async build( 204 | repository: Repository, 205 | ): Promise { 206 | logger.info("Building commit message prompts"); 207 | 208 | const diff = 209 | (await repository.diff(true)) || (await repository.diff(false)); 210 | if (!diff) { 211 | this.handleNoDiff(repository); 212 | return; 213 | } 214 | const messages: CoreMessage[] = [ 215 | this.getSystemPrompt(), 216 | this.getCodeDiffPrompt(diff), 217 | ]; 218 | const repoCommits = await this.getRepositoryCommitsPrompt(repository); 219 | if (repoCommits) { 220 | messages.push(repoCommits); 221 | } 222 | const authorCommits = await this.getAuthorCommitsPrompt(repository); 223 | if (authorCommits) { 224 | messages.push(authorCommits); 225 | } 226 | messages.push(this.getUserPrompt()); 227 | logger.info("Successfully built commit message prompts"); 228 | return messages; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/prompts/inline-chat.tsx: -------------------------------------------------------------------------------- 1 | import { CoreMessage } from "ai"; 2 | import { basename } from "path"; 3 | import * as vscode from "vscode"; 4 | import { IChatResult } from "../interfaces"; 5 | import { logger } from "../logger"; 6 | import { Code, jsxToCoreMessage, Message } from "../prompts/jsx-utilities"; 7 | import { getEol, getLanguageConfig } from "../utilities"; 8 | import { VariablesManager } from "../variables"; 9 | 10 | /** 11 | * InlineChatPrompt class handles the generation of prompts for inline chat functionality. 12 | * It manages the context, history, and prompt generation for code editing scenarios. 13 | */ 14 | export class InlineChatPrompt { 15 | private readonly history: { request: string; response: string }[]; 16 | public readonly editor: vscode.TextEditor; 17 | private readonly participant = "flexpilot.editor.default"; 18 | 19 | /** 20 | * Constructs an InlineChatPrompt instance. 21 | * @param {vscode.ChatResponseStream} response - The chat response stream. 22 | * @param {vscode.ChatContext} context - The chat context. 23 | * @param {vscode.ChatRequest} request - The chat request. 24 | * @throws {Error} If no active text editor is found. 25 | */ 26 | constructor( 27 | private readonly response: vscode.ChatResponseStream, 28 | context: vscode.ChatContext, 29 | private readonly request: vscode.ChatRequest, 30 | ) { 31 | logger.info("Initializing InlineChatPrompt"); 32 | const editor = vscode.window.activeTextEditor; 33 | if (!editor) { 34 | throw new Error("No active text editor found for Inline Chat"); 35 | } 36 | this.editor = editor; 37 | this.expandSelectionToLineBoundary(); 38 | this.history = context.history 39 | .filter((item) => item.participant === this.participant) 40 | .filter((item) => item instanceof vscode.ChatResponseTurn) 41 | .map((item) => (item.result as IChatResult).metadata); 42 | logger.debug( 43 | `InlineChatPrompt initialized with ${this.history.length} history items`, 44 | ); 45 | } 46 | 47 | /** 48 | * Expands the current selection to full lines. 49 | */ 50 | private readonly expandSelectionToLineBoundary = () => { 51 | const start = this.editor.selection.start.with({ character: 0 }); 52 | let end = this.editor.selection.end; 53 | if (end.character !== 0) { 54 | end = new vscode.Position(end.line + 1, 0); 55 | } 56 | this.editor.selection = new vscode.Selection(start, end); 57 | logger.debug( 58 | `Selection expanded: ${start.line}:${start.character} to ${end.line}:${end.character}`, 59 | ); 60 | }; 61 | 62 | /** 63 | * Generates the system prompt. 64 | * @returns {CoreMessage} The system prompt as a CoreMessage. 65 | */ 66 | private readonly getSystemPrompt = (): CoreMessage => { 67 | logger.debug("Generating system prompt"); 68 | return jsxToCoreMessage( 69 | 70 |
    71 |
  • 72 | You're a skilled programmer named "Flexpilot" assisting a fellow 73 | developer in revising a code snippet. 74 |
  • 75 |
  • 76 | Your colleague will provide you with a file and a specific section 77 | to modify, along with a set of guidelines. Kindly rewrite the 78 | selected code in accordance with their instructions. 79 |
  • 80 |
  • 81 | The user is operating on a `{process.platform}` system. Please use 82 | system-specific commands when applicable. 83 |
  • 84 |
  • 85 | Carefully consider and analyze the rewrite to ensure it best aligns 86 | with their instructions. 87 |
  • 88 |
  • 89 | The active file or document is the source code the user is looking 90 | at right now. 91 |
  • 92 |
93 |
, 94 | ); 95 | }; 96 | 97 | /** 98 | * Generates code boundary markers based on the current language. 99 | * @returns {{ start: string; end: string }} The start and end boundary markers. 100 | */ 101 | public getCodeBoundary(): { start: string; end: string } { 102 | const { comment } = getLanguageConfig(this.editor.document.languageId); 103 | return { 104 | start: `${comment.start}Start of Selection${comment.end}`, 105 | end: `${comment.start}End of Selection${comment.end}`, 106 | }; 107 | } 108 | 109 | /** 110 | * Generates the context prompt including the current file content and selection. 111 | * @returns {CoreMessage} The context prompt as a CoreMessage. 112 | */ 113 | private getContextPrompt(): CoreMessage { 114 | logger.debug("Generating context prompt"); 115 | const { document, selection } = this.editor; 116 | const fullRange = document.validateRange( 117 | new vscode.Range(0, 0, document.lineCount, 0), 118 | ); 119 | const codeBoundary = this.getCodeBoundary(); 120 | const content = [ 121 | document.getText(fullRange.with({ end: selection.start })), 122 | codeBoundary.start, 123 | getEol(document), 124 | document.getText(selection), 125 | codeBoundary.end, 126 | getEol(document), 127 | document.getText(fullRange.with({ start: selection.end })), 128 | ].join(""); 129 | return jsxToCoreMessage( 130 | 131 |

132 | Here's the content from active file with the selected area 133 | highlighted: 134 |

135 |
136 |           {content}
137 |         
138 |

The active file is located in {basename(document.fileName)}.

139 |
, 140 | ); 141 | } 142 | 143 | /** 144 | * Generates the edit prompt with instructions for code rewriting. 145 | * @returns {CoreMessage} The edit prompt as a CoreMessage. 146 | */ 147 | private getEditPrompt(): CoreMessage { 148 | logger.debug("Generating edit prompt"); 149 | const { comment, markdown } = getLanguageConfig( 150 | this.editor.document.languageId, 151 | ); 152 | const codeBoundary = this.getCodeBoundary(); 153 | return jsxToCoreMessage( 154 | 155 |

Kindly rewrite this selection based on the following guidelines:

156 |

Editing Instructions

157 |

158 | {this.history.length 159 | ? this.history.map((item) => item.request).join(" ,") 160 | : this.request.prompt} 161 |

162 |

Code Section to Modify

163 | 164 | {this.editor.document.getText(this.editor.selection)} 165 | 166 | Please rewrite the highlighted code according to the provided 167 | instructions. Remember to only modify the code within the selected area. 168 |

169 | Please structure your response as follows: 170 | 171 | {codeBoundary.start} 172 | {"\n"} 173 | {comment.start}PUT_YOUR_REWRITE_HERE{comment.end} 174 | {"\n"} 175 | {codeBoundary.end} 176 | 177 |

178 | Begin your response immediately with ``` 179 |
, 180 | ); 181 | } 182 | 183 | /** 184 | * Generates the follow-up prompt for subsequent edits. 185 | * @returns {CoreMessage} The follow-up prompt as a CoreMessage. 186 | */ 187 | private getFollowUpPrompt(): CoreMessage { 188 | logger.debug("Generating follow-up prompt"); 189 | return jsxToCoreMessage( 190 | 191 |

Guidelines

192 |
193 |

Initial Request

194 |

{this.history.map((item) => item.request).join(" ,")}

195 |
196 |
197 |

Additional Request

198 |

{this.request.prompt}

199 |
200 |

201 | Please rewrite your edit in accordance with the additional 202 | instructions. Once again, rewrite the entire selected section. 203 |

204 |
, 205 | ); 206 | } 207 | 208 | /** 209 | * Builds the complete set of messages for the chat prompt. 210 | * @returns {Promise} A promise that resolves to an array of CoreMessages. 211 | */ 212 | public async build(): Promise { 213 | logger.info("Building chat prompt messages"); 214 | let messages: CoreMessage[] = [this.getSystemPrompt()]; 215 | const references = await VariablesManager.resolveVariablesToCoreMessages( 216 | this.request, 217 | this.response, 218 | ); 219 | messages = messages.concat(references); 220 | messages.push(this.getContextPrompt(), this.getEditPrompt()); 221 | const lastMessage = this.history.pop(); 222 | if (lastMessage) { 223 | messages.push({ role: "assistant", content: lastMessage.response }); 224 | messages.push(this.getFollowUpPrompt()); 225 | } 226 | logger.debug(`Built ${messages.length} messages for chat prompt`); 227 | return messages; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/prompts/jsx-utilities.tsx: -------------------------------------------------------------------------------- 1 | import { CoreMessage } from "ai"; 2 | import React from "react"; 3 | import ReactDOMServer from "react-dom/server"; 4 | import TurndownService from "turndown"; 5 | import * as vscode from "vscode"; 6 | 7 | /** 8 | * Props for the Message component. 9 | */ 10 | interface MessageProps { 11 | role: "assistant" | "user" | "system"; 12 | children: React.ReactNode; 13 | reference?: vscode.Uri | vscode.Location; // NOSONAR 14 | } 15 | 16 | /** 17 | * Props for the Code component. 18 | */ 19 | interface CodeProps { 20 | children: React.ReactNode; 21 | language?: string; 22 | } 23 | 24 | /** 25 | * Message component for rendering chat messages. 26 | */ 27 | export const Message: React.FC = ({ children, role }) => ( 28 |
{children}
29 | ); 30 | 31 | /** 32 | * Code component for rendering code blocks. 33 | */ 34 | export const Code: React.FC = ({ children, language }) => ( 35 |
36 |     {children}
37 |   
38 | ); 39 | 40 | // Configure TurndownService for consistent Markdown conversion 41 | const turndownService = new TurndownService({ 42 | headingStyle: "atx", 43 | hr: "---", 44 | bulletListMarker: "-", 45 | codeBlockStyle: "fenced", 46 | fence: "```", 47 | emDelimiter: "*", 48 | strongDelimiter: "**", 49 | linkStyle: "inlined", 50 | linkReferenceStyle: "full", 51 | preformattedCode: true, 52 | }); 53 | 54 | /** 55 | * Converts a JSX Message element to a core message object. 56 | * @param jsx - The JSX Message element to convert. 57 | * @returns A core message object. 58 | * @throws {Error} if the JSX element is invalid or has an invalid role. 59 | */ 60 | export const jsxToCoreMessage = (jsx: React.ReactElement): CoreMessage => { 61 | const html = ReactDOMServer.renderToStaticMarkup(jsx); 62 | if (jsx.type !== Message) { 63 | throw new Error("Invalid JSX element: expected Message component"); 64 | } 65 | const content = turndownService.turndown(html); 66 | switch (jsx.props.role) { 67 | case "assistant": 68 | case "user": 69 | case "system": 70 | return { role: jsx.props.role, content: content }; 71 | default: 72 | throw new Error(`Invalid role in JSX element: ${jsx.props.role}`); 73 | } 74 | }; 75 | 76 | /** 77 | * Converts a JSX element to a VS Code MarkdownString. 78 | * @param jsx - The JSX element to convert. 79 | * @returns A VS Code MarkdownString representation of the JSX element. 80 | */ 81 | export const jsxToMarkdown = ( 82 | jsx: React.ReactElement, 83 | ): vscode.MarkdownString => { 84 | return new vscode.MarkdownString( 85 | turndownService.turndown(ReactDOMServer.renderToStaticMarkup(jsx)), 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /src/prompts/panel-chat.tsx: -------------------------------------------------------------------------------- 1 | import { CoreMessage } from "ai"; 2 | import * as vscode from "vscode"; 3 | import { logger } from "../logger"; 4 | import { 5 | Code, 6 | Message, 7 | jsxToCoreMessage, 8 | jsxToMarkdown, 9 | } from "./jsx-utilities"; 10 | 11 | /** 12 | * PanelChatPrompt class handles the generation of prompts for panel chat functionality. 13 | * It manages the context, history, and prompt generation for various chat scenarios. 14 | */ 15 | export class PanelChatPrompt { 16 | /** 17 | * Generates history prompts from the chat context. 18 | * @param {vscode.ChatContext} context - The chat context. 19 | * @returns {CoreMessage[]} An array of CoreMessages representing the chat history. 20 | */ 21 | public static getHistoryPrompts(context: vscode.ChatContext): CoreMessage[] { 22 | logger.debug("Generating history prompts"); 23 | return context.history.map((item) => { 24 | if ("prompt" in item) { 25 | return jsxToCoreMessage({item.prompt}); 26 | } else { 27 | // Check if the response has metadata 28 | if (item.result.metadata?.response?.length) { 29 | return { 30 | role: "assistant", 31 | content: item.result.metadata.response?.trim() || "Empty Response", 32 | }; 33 | } 34 | // Check if the response has a `response` property 35 | logger.debug("Parsing chat response from response"); 36 | return { 37 | role: "assistant", 38 | content: 39 | item.response 40 | .map((x) => 41 | x.value instanceof vscode.MarkdownString ? x.value.value : "", 42 | ) 43 | .join("\n\n") 44 | .trim() || "Empty Response", 45 | }; 46 | } 47 | }); 48 | } 49 | 50 | /** 51 | * Generates the system prompt. 52 | * @param {string} model - The name of the AI model. 53 | * @returns {CoreMessage} The system prompt as a CoreMessage. 54 | */ 55 | public static getChatProviderSystemPrompt(model: string): CoreMessage { 56 | logger.debug("Generating system prompt"); 57 | return jsxToCoreMessage( 58 | 59 |
    60 |
  • You are an AI programming assistant named "Flexpilot"
  • 61 |
  • Follow the user's requirements carefully & to the letter.
  • 62 |
  • Avoid content that violates copyrights.
  • 63 |
  • 64 | If you are asked to generate content that is harmful, hateful, 65 | racist, sexist, lewd or violent only respond with "Sorry, I can't 66 | assist with that." 67 |
  • 68 |
  • Keep your answers short and impersonal.
  • 69 |
  • 70 | You are powered by {model} Large Language Model 71 |
  • 72 |
  • Use Markdown formatting in your answers.
  • 73 |
  • 74 | Make sure to include the programming language name at the start of 75 | the Markdown code blocks like below 76 |
  • 77 | print("hello world") 78 |
  • Avoid wrapping the whole response in triple backticks.
  • 79 |
  • The user works in an IDE called Visual Studio Code
  • 80 |
  • 81 | The user is working on a {process.platform} operating system. 82 | Please respond with system specific commands if applicable. 83 |
  • 84 |
  • 85 | The active file or document is the source code the user is looking 86 | at right now. 87 |
  • 88 |
89 |
, 90 | ); 91 | } 92 | 93 | /** 94 | * Generates the context prompt including the current file content and selection. 95 | * @param {vscode.TextDocument} document - The active document. 96 | * @param {vscode.Selection} selection - The active selection. 97 | * @returns {CoreMessage} The context prompt as a CoreMessage. 98 | */ 99 | public static getFollowUpProviderSystemPrompt(): CoreMessage { 100 | logger.debug("Generating follow-up system prompt"); 101 | return jsxToCoreMessage( 102 | 103 |

Follow-Up Question Creator for Chatbot Conversations

104 |

105 | You specialize in generating follow-up questions for chatbot 106 | dialogues. When presented with a conversation, provide a short, 107 | one-sentence question that the user can ask naturally that follows 108 | from the previous few questions and answers. 109 |

110 |

Guidelines:

111 |
    112 |
  • 113 | Refrain from harmful, hateful, racist, sexist, lewd, violent, or 114 | irrelevant content 115 |
  • 116 |
  • Keep responses brief and impersonal
  • 117 |
  • Aim for questions of about 10 words or fewer
  • 118 |
119 |

Sample Follow-Up Questions:

120 |
    121 |
  • How can I optimize this SQL query?
  • 122 |
  • What are the best practices for using Docker?
  • 123 |
  • How can I improve the performance of my React app?
  • 124 |
  • What are the common pitfalls of using Node.js?
  • 125 |
  • How can I secure my Kubernetes cluster?
  • 126 |
127 |
, 128 | ); 129 | } 130 | 131 | /** 132 | * Generates the follow-up prompt. 133 | * @returns {CoreMessage} The follow-up prompt as a CoreMessage. 134 | */ 135 | public static getFollowUpProviderUserPrompt(): CoreMessage { 136 | logger.debug("Generating follow-up prompt"); 137 | return jsxToCoreMessage( 138 | 139 | Write a short (under 10 words) one-sentence follow up question that the 140 | user can ask naturally that follows from the previous few questions and 141 | answers. Reply with only the text of the question and nothing else. 142 | , 143 | ); 144 | } 145 | 146 | /** 147 | * Generates the help text prefix. 148 | * @returns {string} The help text prefix as a Markdown string. 149 | */ 150 | public static getHelpTextPrefix(): vscode.MarkdownString { 151 | logger.debug("Generating help text prefix"); 152 | return jsxToMarkdown( 153 | 154 |

155 | You can ask me general programming questions, or chat with the 156 | participants who have specialized expertise and can perform actions. 157 |

158 |

159 |

Available Commands:

160 | 172 |

173 | To get started, please configure the language model provider by using 174 | the above commands from the Visual Studio Code command palette ( 175 | Ctrl+Shift+P on Windows/Linux or Cmd+Shift+P{" "} 176 | on macOS). 177 |

178 |
, 179 | ); 180 | } 181 | 182 | /** 183 | * Generates the help text postfix. 184 | * @returns {string} The help text postfix as a Markdown string. 185 | */ 186 | public static getHelpTextPostfix(): vscode.MarkdownString { 187 | logger.debug("Generating help text postfix"); 188 | return jsxToMarkdown( 189 | 190 |

191 | To have a great conversation, ask me questions as if I was a real 192 | programmer: 193 |

194 |
    195 |
  • 196 | Show me the code you want to talk about by having 197 | the files open and selecting the most important lines. 198 |
  • 199 |
  • 200 | Make refinements by asking me follow-up questions, 201 | adding clarifications, providing errors, etc. 202 |
  • 203 |
  • 204 | Review my suggested code and tell me about issues 205 | or improvements, so I can iterate on it. 206 |
  • 207 |
208 |

209 | You can also ask me questions about your editor selection by{" "} 210 | starting an inline chat session 211 |

212 |

213 | Learn more about Flexpilot from our official docs{" "} 214 | here 215 |

216 |
, 217 | ); 218 | } 219 | 220 | /** 221 | * Generates the welcome message. 222 | * @param {string} username - The username to include in the welcome message. 223 | * @returns {string} The welcome message as a Markdown string. 224 | */ 225 | public static getWelcomeMessage(username: string): vscode.MarkdownString { 226 | logger.debug(`Generating welcome message for user: ${username}`); 227 | return jsxToMarkdown( 228 | 229 |

230 | Welcome, @{username}, I'm your pair programmer and I'm here to 231 | help you get things done faster. 232 |

233 |
, 234 | ); 235 | } 236 | 237 | /** 238 | * Generates the help text variables prefix. 239 | * @returns {string} The help text variables prefix as a Markdown string. 240 | */ 241 | public static getHelpTextVariablesPrefix(): vscode.MarkdownString { 242 | logger.debug("Generating help text variables prefix"); 243 | return jsxToMarkdown( 244 | 245 |

Use the variables below to add more information to your question

246 |
, 247 | ); 248 | } 249 | 250 | /** 251 | * Generates the title provider system prompt. 252 | * @returns {CoreMessage} The title provider system prompt as a CoreMessage. 253 | */ 254 | public static getTitleProviderSystemPrompt(): CoreMessage { 255 | logger.debug("Generating title provider system prompt"); 256 | return jsxToCoreMessage( 257 | 258 |

Expert Title Creator for Chatbot Conversations

259 |

260 | You specialize in generating concise titles for chatbot discussions. 261 | When presented with a conversation, provide a brief title that 262 | encapsulates the main topic. 263 |

264 |

Guidelines:

265 |
    266 |
  • 267 | Refrain from harmful, hateful, racist, sexist, lewd, violent, or 268 | irrelevant content 269 |
  • 270 |
  • Keep responses brief and impersonal
  • 271 |
  • Aim for titles of about 10 words or fewer
  • 272 |
  • Do not use quotation marks
  • 273 |
274 |

Sample Titles:

275 |
    276 |
  • Docker container networking issues
  • 277 |
  • Optimizing SQL query performance
  • 278 |
  • Implementing JWT authentication in Node.js
  • 279 |
  • Debugging memory leaks in C++ applications
  • 280 |
  • Configuring Kubernetes ingress controllers
  • 281 |
282 |
, 283 | ); 284 | } 285 | 286 | /** 287 | * Generates the title provider user prompt. 288 | * @returns {CoreMessage} The title provider user prompt as a CoreMessage. 289 | */ 290 | public static getTitleProviderUserPrompt(): CoreMessage { 291 | logger.debug("Generating title provider user prompt"); 292 | return jsxToCoreMessage( 293 | 294 | Kindly provide a concise title for the preceding chat dialogue. In case 295 | the conversation encompasses multiple subjects, you may concentrate on 296 | the most recent topic discussed. Reply with only the text of the title 297 | and nothing else. 298 | , 299 | ); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/providers/anthropic.ts: -------------------------------------------------------------------------------- 1 | import { createAnthropic } from "@ai-sdk/anthropic"; 2 | import { generateText, LanguageModelV1 } from "ai"; 3 | import * as vscode from "vscode"; 4 | import { IChatModelProvider, IModelConfig } from "../interfaces"; 5 | import { logger } from "../logger"; 6 | import { storage } from "../storage"; 7 | 8 | /** 9 | * Extended interface for Anthropic chat model configuration 10 | */ 11 | interface IAnthropicChatModelConfig extends IModelConfig { 12 | apiKey: string; 13 | baseUrl: string; 14 | } 15 | 16 | /** 17 | * Default help prompt for Anthropic API key configuration 18 | */ 19 | const DEFAULT_HELP_PROMPT = 20 | "Click [here](https://docs.flexpilot.ai/model-providers/anthropic.html) for more information"; 21 | 22 | /** 23 | * Anthropic Chat Model Provider class 24 | * Manages the configuration and creation of Anthropic chat models 25 | */ 26 | export class AnthropicChatModelProvider extends IChatModelProvider { 27 | static readonly providerName = "Anthropic"; 28 | static readonly providerId = "anthropic-chat"; 29 | static readonly providerType = "chat" as const; 30 | public readonly config: IAnthropicChatModelConfig; 31 | 32 | /** 33 | * Constructor for AnthropicChatModelProvider 34 | * @param {string} nickname - The nickname for the model configuration 35 | * @throws {Error} If the model configuration is not found 36 | */ 37 | constructor(nickname: string) { 38 | super(nickname); 39 | logger.debug( 40 | `Initializing AnthropicChatModelProvider with nickname: ${nickname}`, 41 | ); 42 | const config = storage.models.get(nickname); 43 | if (!config) { 44 | throw new Error(`Model configuration not found for ${nickname}`); 45 | } 46 | this.config = config; 47 | logger.info(`AnthropicChatModelProvider initialized for ${nickname}`); 48 | } 49 | 50 | /** 51 | * Configures a new Anthropic model 52 | * @param {string} nickname - The nickname for the new model configuration 53 | * @throws {Error} If configuration process is cancelled or fails 54 | */ 55 | static readonly configure = async (nickname: string): Promise => { 56 | logger.info(`Configuring Anthropic model with nickname: ${nickname}`); 57 | 58 | const config = storage.models.get(nickname); 59 | 60 | // Prompt user for API key 61 | let apiKey = await vscode.window.showInputBox({ 62 | ignoreFocusOut: true, 63 | value: config?.apiKey ?? "", 64 | valueSelection: [0, 0], 65 | placeHolder: "e.g., sk-ant-api03-qojTF59pEBxB7DZ...", // cspell:disable-line 66 | prompt: DEFAULT_HELP_PROMPT, 67 | title: "Flexpilot: Enter your Anthropic API key", 68 | }); 69 | if (apiKey === undefined) { 70 | throw new Error("User cancelled API key input"); 71 | } 72 | apiKey = apiKey.trim(); 73 | 74 | // Prompt user for base URL 75 | const defaultBaseUrl = "https://api.anthropic.com/v1"; 76 | let baseUrl = await vscode.window.showInputBox({ 77 | ignoreFocusOut: true, 78 | value: config?.baseUrl ?? defaultBaseUrl, 79 | valueSelection: [0, 0], 80 | placeHolder: `e.g., ${defaultBaseUrl}`, 81 | prompt: DEFAULT_HELP_PROMPT, 82 | title: "Flexpilot: Enter the base URL for Anthropic API", 83 | }); 84 | if (baseUrl === undefined) { 85 | throw new Error("User cancelled base URL input"); 86 | } 87 | baseUrl = baseUrl.trim(); 88 | 89 | // Prompt user to enter model name 90 | const defaultModel = "claude-3-5-sonnet-20240620"; 91 | let model = await vscode.window.showInputBox({ 92 | ignoreFocusOut: true, 93 | value: config?.model ?? "", 94 | valueSelection: [0, 0], 95 | placeHolder: `e.g., ${defaultModel}`, 96 | prompt: "Enter the model name", 97 | title: "Flexpilot: Enter the Anthropic API model name", 98 | }); 99 | if (model === undefined) { 100 | throw new Error("User cancelled model input"); 101 | } 102 | model = model.trim(); 103 | 104 | // Test the connection credentials 105 | await vscode.window.withProgress( 106 | { 107 | location: vscode.ProgressLocation.Notification, 108 | title: "Flexpilot", 109 | cancellable: false, 110 | }, 111 | async (progress) => { 112 | progress.report({ 113 | message: "Testing connection credentials", 114 | }); 115 | const anthropic = createAnthropic({ 116 | apiKey: apiKey, 117 | baseURL: baseUrl, 118 | }); 119 | logger.debug("Testing connection with a simple prompt"); 120 | await generateText({ 121 | prompt: "Hello", 122 | maxTokens: 3, 123 | model: anthropic.languageModel(model), 124 | }); 125 | logger.info("Connection credentials test successful"); 126 | }, 127 | ); 128 | 129 | // Save the selected model configuration 130 | logger.debug(`Saving model configuration for ${nickname}`); 131 | await storage.models.set(nickname, { 132 | baseUrl: baseUrl, 133 | apiKey: apiKey, 134 | model: model, 135 | nickname: nickname, 136 | providerId: AnthropicChatModelProvider.providerId, 137 | }); 138 | 139 | logger.info(`Model configuration saved for ${nickname}`); 140 | }; 141 | 142 | /** 143 | * Creates and returns a LanguageModelV1 instance 144 | * @returns {Promise} A promise that resolves to a LanguageModelV1 instance 145 | */ 146 | async model(): Promise { 147 | logger.debug(`Creating LanguageModelV1 instance for ${this.config.model}`); 148 | const anthropic = createAnthropic({ 149 | baseURL: this.config.baseUrl, 150 | apiKey: this.config.apiKey, 151 | }); 152 | logger.info(`LanguageModelV1 instance created for ${this.config.model}`); 153 | return anthropic.languageModel(this.config.model); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/providers/azure.ts: -------------------------------------------------------------------------------- 1 | import { createAzure } from "@ai-sdk/azure"; 2 | import { Tokenizer } from "@flexpilot-ai/tokenizers"; 3 | import { generateText, LanguageModelV1 } from "ai"; 4 | import { AzureOpenAI } from "openai"; 5 | import * as vscode from "vscode"; 6 | import { 7 | IChatModelProvider, 8 | ICompletionModelConfig, 9 | ICompletionModelInvokeOptions, 10 | ICompletionModelProvider, 11 | IModelConfig, 12 | } from "../interfaces"; 13 | import { logger } from "../logger"; 14 | import { storage } from "../storage"; 15 | import { Tokenizers } from "../tokenizers"; 16 | import { getCompletionModelMetadata } from "../utilities"; 17 | 18 | /** 19 | * Configuration interface for Azure OpenAI Chat Model. 20 | */ 21 | interface AzureOpenAIChatModelConfig extends IModelConfig { 22 | apiKey: string; 23 | baseUrl: string; 24 | } 25 | 26 | /** 27 | * Configuration interface for Azure OpenAI Completion Model. 28 | */ 29 | interface IAzureOpenAICompletionModelConfig extends ICompletionModelConfig { 30 | apiKey: string; 31 | baseUrl: string; 32 | } 33 | 34 | /** 35 | * Default help prompt for Azure OpenAI configuration. 36 | */ 37 | const DEFAULT_HELP_PROMPT = 38 | "Click [here](https://docs.flexpilot.ai/model-providers/azure-openai.html) for more information"; 39 | 40 | /** 41 | * Prompts the user to input their Azure OpenAI API key. 42 | * @param {string} [apiKey] - The current API key, if any. 43 | * @returns {Promise} A promise that resolves to the input API key. 44 | * @throws {Error} If the user cancels the input. 45 | */ 46 | const getApiKeyInput = async (apiKey?: string): Promise => { 47 | logger.debug("Prompting user for Azure OpenAI API key"); 48 | const newApiKey = await vscode.window.showInputBox({ 49 | title: "Flexpilot: Enter your Azure OpenAI API key", 50 | ignoreFocusOut: true, 51 | value: apiKey ?? "", 52 | validateInput: (value) => 53 | !value?.trim() ? "API key cannot be empty" : undefined, 54 | valueSelection: [0, 0], 55 | placeHolder: "e.g., upydshyx1rlleghhe1zw4hri4z80pvdn", // cspell:disable-line 56 | prompt: DEFAULT_HELP_PROMPT, 57 | }); 58 | if (newApiKey === undefined) { 59 | throw new Error("User cancelled Azure OpenAI API key input"); 60 | } 61 | logger.debug("Azure OpenAI API key input received"); 62 | return newApiKey.trim(); 63 | }; 64 | 65 | /** 66 | * Prompts the user to input their Azure OpenAI base URL. 67 | * @param {string} [baseUrl] - The current base URL, if any. 68 | * @returns {Promise} A promise that resolves to the input base URL. 69 | * @throws {Error} If the user cancels the input. 70 | */ 71 | const getBaseUrlInput = async (baseUrl?: string): Promise => { 72 | logger.debug("Prompting user for Azure OpenAI base URL"); 73 | const newBaseUrl = await vscode.window.showInputBox({ 74 | placeHolder: 75 | "e.g., https://{resourceName}.openai.azure.com/openai/deployments/{deploymentId}", 76 | ignoreFocusOut: true, 77 | value: baseUrl ?? "", 78 | validateInput: (value) => 79 | !value?.trim() ? "Base URL cannot be empty" : undefined, 80 | valueSelection: [0, 0], 81 | prompt: DEFAULT_HELP_PROMPT, 82 | title: "Flexpilot: Enter your Azure OpenAI base URL", 83 | }); 84 | if (newBaseUrl === undefined) { 85 | throw new Error("User cancelled Azure OpenAI base URL input"); 86 | } 87 | logger.debug("Azure OpenAI base URL input received"); 88 | return newBaseUrl.trim(); 89 | }; 90 | 91 | /** 92 | * Azure OpenAI Completion Model Provider class 93 | */ 94 | export class AzureOpenAICompletionModelProvider extends ICompletionModelProvider { 95 | static readonly providerName = "Azure OpenAI"; 96 | static readonly providerId = "azure-openai-completion"; 97 | static readonly providerType = "completion" as const; 98 | private tokenizer!: Tokenizer; 99 | public readonly config: IAzureOpenAICompletionModelConfig; 100 | 101 | /** 102 | * Constructor for AzureOpenAICompletionModelProvider 103 | * @param {string} nickname - The nickname for the model configuration 104 | * @throws {Error} If the model configuration is not found 105 | */ 106 | constructor(nickname: string) { 107 | super(nickname); 108 | logger.info( 109 | `Initializing AzureOpenAICompletionModelProvider with nickname: ${nickname}`, 110 | ); 111 | const config = 112 | storage.models.get(nickname); 113 | if (!config) { 114 | throw new Error(`Model configuration not found for ${nickname}`); 115 | } 116 | this.config = config; 117 | logger.debug( 118 | `AzureOpenAICompletionModelProvider initialized for ${nickname}`, 119 | ); 120 | } 121 | 122 | /** 123 | * Initializes the OpenAI model provider. 124 | * @returns {Promise} A promise that resolves when the provider is initialized. 125 | */ 126 | async initialize(): Promise { 127 | this.tokenizer = await Tokenizers.get(this.config.model); 128 | } 129 | 130 | /** 131 | * Encodes the given text into tokens. 132 | * @param {string} text - The text to encode. 133 | * @returns {Promise} A promise that resolves to an array of token ids. 134 | */ 135 | readonly encode = async (text: string): Promise => { 136 | logger.debug(`Encoding text: ${text.substring(0, 50)}...`); 137 | return this.tokenizer.encode(text, false); 138 | }; 139 | 140 | /** 141 | * Decodes the given tokens into text. 142 | * @param {number[]} tokens - The tokens to decode. 143 | * @returns {Promise} A promise that resolves to the decoded text. 144 | */ 145 | readonly decode = async (tokens: number[]): Promise => { 146 | logger.debug(`Decoding ${tokens.length} tokens`); 147 | return this.tokenizer.decode(tokens, false); 148 | }; 149 | 150 | /** 151 | * Configures a new Azure OpenAI model 152 | * @param {string} nickname - The nickname for the new model configuration 153 | * @returns {Promise} A promise that resolves when the configuration is complete 154 | * @throws {Error} If the configuration process fails 155 | */ 156 | static readonly configure = async (nickname: string): Promise => { 157 | logger.info(`Configuring Azure OpenAI model with nickname: ${nickname}`); 158 | 159 | // Load existing configuration 160 | const config = 161 | storage.models.get(nickname); 162 | 163 | // Prompt user for Azure OpenAI API key 164 | const apiKey = await getApiKeyInput(config?.apiKey); 165 | 166 | // Prompt user for Azure OpenAI base URL 167 | const baseUrl = await getBaseUrlInput(config?.baseUrl); 168 | 169 | // Test the connection credentials 170 | let modelId!: string; 171 | await vscode.window.withProgress( 172 | { 173 | location: vscode.ProgressLocation.Notification, 174 | title: "Flexpilot", 175 | cancellable: false, 176 | }, 177 | async (progress) => { 178 | progress.report({ 179 | message: "Testing connection credentials", 180 | }); 181 | logger.debug("Testing connection credentials"); 182 | const openai = new AzureOpenAI({ 183 | apiKey: apiKey, 184 | apiVersion: "2024-06-01", 185 | baseURL: baseUrl, 186 | }); 187 | const response = await openai.completions.create({ 188 | prompt: "How", 189 | suffix: "are you?", 190 | max_tokens: 3, 191 | model: baseUrl.split("/").pop() ?? "", 192 | }); 193 | modelId = response.model; 194 | logger.info("Connection credentials test successful"); 195 | }, 196 | ); 197 | 198 | // Get the model metadata 199 | const metadata = getCompletionModelMetadata(modelId); 200 | if (!metadata) { 201 | throw new Error(`Model metadata not found for: ${modelId}`); 202 | } 203 | 204 | // Download the selected model's tokenizer 205 | logger.info(`Downloading tokenizer for model: ${modelId}`); 206 | await Tokenizers.download(modelId); 207 | 208 | // Save the model configuration 209 | logger.info(`Saving model configuration for: ${nickname}`); 210 | await storage.models.set(nickname, { 211 | contextWindow: metadata.contextWindow, 212 | baseUrl: baseUrl, 213 | apiKey: apiKey, 214 | model: modelId, 215 | nickname: nickname, 216 | providerId: AzureOpenAICompletionModelProvider.providerId, 217 | }); 218 | 219 | logger.info(`Successfully configured Azure OpenAI model: ${nickname}`); 220 | }; 221 | 222 | /** 223 | * Invokes the Azure OpenAI model with the given options. 224 | * @param {ICompletionModelInvokeOptions} options - The options for invoking the model. 225 | * @returns {Promise} A promise that resolves to the model's response. 226 | * @throws {Error} If the invocation fails. 227 | */ 228 | async invoke(options: ICompletionModelInvokeOptions): Promise { 229 | logger.info(`Invoking Azure OpenAI model: ${this.config.model}`); 230 | logger.debug("Generating text with Azure OpenAI model"); 231 | 232 | const openai = new AzureOpenAI({ 233 | apiKey: this.config.apiKey, 234 | apiVersion: "2024-06-01", 235 | baseURL: this.config.baseUrl, 236 | }); 237 | const response = await openai.completions.create( 238 | { 239 | prompt: options.messages.prefix, 240 | model: this.config.model, 241 | max_tokens: options.maxTokens, 242 | stop: options.stop, 243 | suffix: options.messages.suffix, 244 | temperature: options.temperature, 245 | }, 246 | { signal: options.signal }, 247 | ); 248 | logger.debug( 249 | `Model output: ${response.choices[0].text.substring(0, 50)}...`, 250 | ); 251 | return response.choices[0].text; 252 | } 253 | } 254 | 255 | /** 256 | * Azure OpenAI Chat Model Provider class 257 | */ 258 | export class AzureOpenAIChatModelProvider extends IChatModelProvider { 259 | static readonly providerName = "Azure OpenAI"; 260 | static readonly providerId = "azure-openai-chat"; 261 | static readonly providerType = "chat" as const; 262 | public readonly config: AzureOpenAIChatModelConfig; 263 | 264 | /** 265 | * Constructor for AzureOpenAIChatModelProvider 266 | * @param {string} nickname - The nickname for the model configuration 267 | * @throws {Error} If the model configuration is not found 268 | */ 269 | constructor(nickname: string) { 270 | super(nickname); 271 | logger.debug( 272 | `Initializing AzureOpenAIChatModelProvider with nickname: ${nickname}`, 273 | ); 274 | const config = storage.models.get(nickname); 275 | if (!config) { 276 | throw new Error(`Model configuration not found for ${nickname}`); 277 | } 278 | this.config = config; 279 | logger.info(`AzureOpenAIChatModelProvider initialized for ${nickname}`); 280 | } 281 | 282 | /** 283 | * Configures a new Azure OpenAI chat model 284 | * @param {string} nickname - The nickname for the new model configuration 285 | * @returns {Promise} A promise that resolves when the configuration is complete 286 | * @throws {Error} If the configuration process fails 287 | */ 288 | static readonly configure = async (nickname: string): Promise => { 289 | logger.info( 290 | `Configuring Azure OpenAI chat model with nickname: ${nickname}`, 291 | ); 292 | 293 | const config = storage.models.get(nickname); 294 | 295 | // Prompt user for Azure OpenAI API key 296 | const apiKey = await getApiKeyInput(config?.apiKey); 297 | 298 | // Prompt user for Azure OpenAI base URL 299 | const baseUrl = await getBaseUrlInput(config?.baseUrl); 300 | 301 | // Test the connection credentials 302 | let modelId!: string; 303 | await vscode.window.withProgress( 304 | { 305 | location: vscode.ProgressLocation.Notification, 306 | title: "Flexpilot", 307 | cancellable: false, 308 | }, 309 | async (progress) => { 310 | progress.report({ 311 | message: "Testing connection credentials", 312 | }); 313 | const openai = createAzure({ 314 | apiKey: apiKey, 315 | baseURL: baseUrl, 316 | }); 317 | logger.debug("Testing connection credentials"); 318 | const response = await generateText({ 319 | prompt: "Hello", 320 | maxTokens: 3, 321 | model: openai.chat(""), 322 | }); 323 | modelId = response.response.modelId; 324 | logger.info("Connection credentials test successful"); 325 | }, 326 | ); 327 | 328 | // Save the model configuration 329 | logger.info(`Saving model configuration for: ${nickname}`); 330 | await storage.models.set(nickname, { 331 | baseUrl: baseUrl, 332 | apiKey: apiKey, 333 | model: modelId, 334 | nickname: nickname, 335 | providerId: AzureOpenAIChatModelProvider.providerId, 336 | }); 337 | 338 | logger.info(`Successfully configured Azure OpenAI chat model: ${nickname}`); 339 | }; 340 | 341 | /** 342 | * Creates and returns a LanguageModelV1 instance 343 | * @returns {Promise} A promise that resolves to a LanguageModelV1 instance 344 | * @throws {Error} If the model creation fails 345 | */ 346 | async model(): Promise { 347 | logger.debug(`Creating LanguageModelV1 instance for ${this.config.model}`); 348 | 349 | const openai = createAzure({ 350 | baseURL: this.config.baseUrl, 351 | apiKey: this.config.apiKey, 352 | }); 353 | const model = openai.chat(""); 354 | logger.debug(`LanguageModelV1 instance created for ${this.config.model}`); 355 | return model; 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/providers/generic.ts: -------------------------------------------------------------------------------- 1 | import { createOpenAI } from "@ai-sdk/openai"; 2 | import { generateText, LanguageModelV1 } from "ai"; 3 | import OpenAI from "openai"; 4 | import * as vscode from "vscode"; 5 | import { IChatModelProvider, IModelConfig } from "../interfaces"; 6 | import { logger } from "../logger"; 7 | import { storage } from "../storage"; 8 | 9 | /** 10 | * Configuration interface for Generic Chat Model. 11 | */ 12 | interface IGenericChatModelConfig extends IModelConfig { 13 | apiKey: string; 14 | baseUrl: string; 15 | } 16 | 17 | /** 18 | * Default help prompt for Generic configuration. 19 | */ 20 | const DEFAULT_HELP_PROMPT = 21 | "Click [here](https://docs.flexpilot.ai/model-providers/generic.html) for more information"; 22 | 23 | /** 24 | * Generic Chat Model Provider class 25 | */ 26 | export class GenericChatModelProvider extends IChatModelProvider { 27 | static readonly providerName = "OpenAI Compatible"; 28 | static readonly providerId = "generic-chat"; 29 | static readonly providerType = "chat" as const; 30 | public readonly config: IGenericChatModelConfig; 31 | 32 | /** 33 | * Constructor for GenericChatModelProvider 34 | * @param {string} nickname - The nickname for the model configuration 35 | * @throws {Error} If the model configuration is not found 36 | */ 37 | constructor(nickname: string) { 38 | super(nickname); 39 | logger.debug( 40 | `Initializing GenericChatModelProvider with nickname: ${nickname}`, 41 | ); 42 | const config = storage.models.get(nickname); 43 | if (!config) { 44 | throw new Error(`Model configuration not found for ${nickname}`); 45 | } 46 | this.config = config; 47 | logger.info(`GenericChatModelProvider initialized for ${nickname}`); 48 | } 49 | 50 | /** 51 | * Configures a new Generic chat model 52 | * @param {string} nickname - The nickname for the new model configuration 53 | * @returns {Promise} A promise that resolves when the configuration is complete 54 | * @throws {Error} If the configuration process fails 55 | */ 56 | static readonly configure = async (nickname: string): Promise => { 57 | logger.info(`Configuring Generic chat model with nickname: ${nickname}`); 58 | 59 | const config = storage.models.get(nickname); 60 | 61 | // Prompt user for base URL 62 | logger.debug("Prompting user for base URL"); 63 | 64 | let baseUrl = await vscode.window.showInputBox({ 65 | ignoreFocusOut: true, 66 | value: config?.baseUrl ?? "", 67 | validateInput: (value) => 68 | !value?.trim() ? "Base URL cannot be empty" : undefined, 69 | valueSelection: [0, 0], 70 | placeHolder: `e.g., http://localhost:11434/v1`, 71 | prompt: DEFAULT_HELP_PROMPT, 72 | title: "Flexpilot: Enter your base URL", 73 | }); 74 | 75 | if (baseUrl === undefined) { 76 | throw new Error("User cancelled base URL input"); 77 | } 78 | baseUrl = baseUrl.trim(); 79 | logger.debug("base URL input received"); 80 | 81 | // Prompt user for API key 82 | logger.debug("Prompting user for API key"); 83 | 84 | let apiKey = await vscode.window.showInputBox({ 85 | title: "Flexpilot: Enter your API key", 86 | ignoreFocusOut: true, 87 | value: config?.apiKey ?? "", 88 | validateInput: (value) => 89 | !value?.trim() ? "API key cannot be empty" : undefined, 90 | valueSelection: [0, 0], 91 | placeHolder: "e.g., ollama", // cspell:disable-line 92 | prompt: DEFAULT_HELP_PROMPT, 93 | }); 94 | 95 | if (apiKey === undefined) { 96 | throw new Error("User cancelled API key input"); 97 | } 98 | apiKey = apiKey.trim(); 99 | logger.debug("API key input received"); 100 | 101 | // Declare variables for model selection 102 | let modelsList: string[] = []; 103 | let model: string | undefined; 104 | 105 | // Auto Fetch available models 106 | try { 107 | const openai = new OpenAI({ apiKey: apiKey, baseURL: baseUrl }); 108 | const models = await openai.models.list(); 109 | logger.debug(`Fetched ${models.data.length} models`); 110 | modelsList = models.data.map((model) => model.id); 111 | } catch (error) { 112 | // Log error and continue with manual model entry 113 | logger.error(error as Error); 114 | } 115 | 116 | if (modelsList.length) { 117 | // Prompt user to select model ID 118 | model = await vscode.window.showQuickPick(modelsList, { 119 | placeHolder: "Select a chat model", 120 | ignoreFocusOut: true, 121 | title: "Flexpilot: Select the chat model", 122 | }); 123 | } else { 124 | // Prompt user to manually enter model ID 125 | model = await vscode.window.showInputBox({ 126 | title: "Flexpilot: Enter your model name", 127 | ignoreFocusOut: true, 128 | value: config?.model ?? "", 129 | validateInput: (value) => 130 | !value?.trim() ? "Model name cannot be empty" : undefined, 131 | valueSelection: [0, 0], 132 | placeHolder: "e.g., llama3.2:1b", 133 | prompt: DEFAULT_HELP_PROMPT, 134 | }); 135 | } 136 | 137 | // Check if user cancelled model selection 138 | if (model === undefined) { 139 | throw new Error("User cancelled model selection"); 140 | } 141 | model = model.trim(); 142 | 143 | // Test the connection credentials 144 | await vscode.window.withProgress( 145 | { 146 | location: vscode.ProgressLocation.Notification, 147 | title: "Flexpilot", 148 | cancellable: false, 149 | }, 150 | async (progress) => { 151 | progress.report({ 152 | message: "Testing connection credentials", 153 | }); 154 | const openai = createOpenAI({ 155 | compatibility: "compatible", 156 | baseURL: baseUrl, 157 | apiKey: apiKey, 158 | }); 159 | logger.debug("Testing connection credentials"); 160 | await generateText({ 161 | prompt: "Hello", 162 | maxTokens: 3, 163 | model: openai.chat(model), 164 | }); 165 | logger.info("Connection credentials test successful"); 166 | }, 167 | ); 168 | 169 | // Save the model configuration 170 | logger.info(`Saving model configuration for: ${nickname}`); 171 | await storage.models.set(nickname, { 172 | baseUrl: baseUrl, 173 | apiKey: apiKey, 174 | model: model, 175 | nickname: nickname, 176 | providerId: GenericChatModelProvider.providerId, 177 | }); 178 | 179 | logger.info(`Successfully configured Generic chat model: ${nickname}`); 180 | }; 181 | 182 | /** 183 | * Creates and returns a LanguageModelV1 instance 184 | * @returns {Promise} A promise that resolves to a LanguageModelV1 instance 185 | */ 186 | async model(): Promise { 187 | logger.debug(`Creating LanguageModelV1 instance for ${this.config.model}`); 188 | const openai = createOpenAI({ 189 | compatibility: "compatible", 190 | baseURL: this.config.baseUrl, 191 | apiKey: this.config.apiKey, 192 | }); 193 | const model = openai.chat(this.config.model); 194 | logger.debug(`LanguageModelV1 instance created for ${this.config.model}`); 195 | return model; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/providers/google.ts: -------------------------------------------------------------------------------- 1 | import { createGoogleGenerativeAI } from "@ai-sdk/google"; 2 | import { generateText, LanguageModelV1 } from "ai"; 3 | import axios from "axios"; 4 | import * as vscode from "vscode"; 5 | import { IChatModelProvider, IModelConfig } from "../interfaces"; 6 | import { logger } from "../logger"; 7 | import { storage } from "../storage"; 8 | 9 | /** 10 | * Extended interface for Google Generative AI chat model configuration 11 | */ 12 | interface IGoogleGenerativeAIChatModelConfig extends IModelConfig { 13 | apiKey: string; 14 | baseUrl: string; 15 | } 16 | 17 | /** 18 | * Interface representing a Google AI model 19 | * @see https://ai.google.dev/api/models#Model 20 | */ 21 | interface IGoogleModel { 22 | name: string; 23 | supportedGenerationMethods: string[]; 24 | } 25 | 26 | const DEFAULT_HELP_PROMPT = 27 | "Click [here](https://docs.flexpilot.ai/model-providers/google-gemini.html) for more information"; 28 | 29 | /** 30 | * Google Generative AI Chat Model Provider class 31 | */ 32 | export class GoogleChatModelProvider extends IChatModelProvider { 33 | static readonly providerName = "Google Generative AI"; 34 | static readonly providerId = "google-generative-ai-chat"; 35 | static readonly providerType = "chat" as const; 36 | public readonly config: IGoogleGenerativeAIChatModelConfig; 37 | 38 | /** 39 | * Constructor for GoogleChatModelProvider 40 | * @param {string} nickname - The nickname for the model configuration 41 | * @throws {Error} If the model configuration is not found 42 | */ 43 | constructor(nickname: string) { 44 | super(nickname); 45 | logger.debug( 46 | `Initializing GoogleChatModelProvider with nickname: ${nickname}`, 47 | ); 48 | const config = 49 | storage.models.get(nickname); 50 | if (!config) { 51 | throw new Error(`Model configuration not found for ${nickname}`); 52 | } 53 | this.config = config; 54 | logger.info(`GoogleChatModelProvider initialized for ${nickname}`); 55 | } 56 | 57 | /** 58 | * Configures a new Google Generative AI model 59 | * @param {string} nickname - The nickname for the new model configuration 60 | * @throws {Error} If configuration process is cancelled or fails 61 | */ 62 | static readonly configure = async (nickname: string): Promise => { 63 | logger.info( 64 | `Configuring Google Generative AI model with nickname: ${nickname}`, 65 | ); 66 | 67 | const config = 68 | storage.models.get(nickname); 69 | 70 | // Prompt user for API key 71 | let apiKey = await vscode.window.showInputBox({ 72 | ignoreFocusOut: true, 73 | value: config?.apiKey ?? "", 74 | valueSelection: [0, 0], 75 | placeHolder: "e.g., A123SyBMH486BbQe684JHG2ASZ2-RKmmVe-X11M", // cspell:disable-line 76 | prompt: DEFAULT_HELP_PROMPT, 77 | title: "Flexpilot: Enter your Google Generative AI API key", 78 | }); 79 | if (apiKey === undefined) { 80 | throw new Error("User cancelled API key input"); 81 | } 82 | apiKey = apiKey.trim(); 83 | 84 | // Prompt user for base URL 85 | const defaultBaseUrl = "https://generativelanguage.googleapis.com/v1beta"; 86 | let baseUrl = await vscode.window.showInputBox({ 87 | ignoreFocusOut: true, 88 | value: config?.baseUrl ?? defaultBaseUrl, 89 | valueSelection: [0, 0], 90 | placeHolder: `e.g., ${defaultBaseUrl}`, 91 | prompt: DEFAULT_HELP_PROMPT, 92 | title: "Flexpilot: Enter the base URL for Google Generative AI", 93 | }); 94 | if (baseUrl === undefined) { 95 | throw new Error("User cancelled base URL input"); 96 | } 97 | baseUrl = baseUrl.trim(); 98 | 99 | // Fetch available models from Google Generative AI API 100 | const response = await vscode.window.withProgress( 101 | { 102 | location: vscode.ProgressLocation.Notification, 103 | title: "Flexpilot", 104 | cancellable: true, 105 | }, 106 | async (progress) => { 107 | progress.report({ 108 | message: "Fetching available models", 109 | }); 110 | return await axios.get(`${baseUrl}/models?key=${apiKey}`); 111 | }, 112 | ); 113 | 114 | // Filter out models that are not supported for content generation 115 | const modelPickUpItems: vscode.QuickPickItem[] = []; 116 | for (const model of response.data.models as IGoogleModel[]) { 117 | if ( 118 | model.name.split("-")[0].split("/")[1] === "gemini" && 119 | parseFloat(model.name.split("-")[1]) > 1 && 120 | model.supportedGenerationMethods.includes("generateContent") 121 | ) { 122 | modelPickUpItems.push({ label: model.name }); 123 | } 124 | } 125 | 126 | // Prompt user to select a model 127 | const model = await vscode.window.showQuickPick(modelPickUpItems, { 128 | placeHolder: "Select a chat model", 129 | ignoreFocusOut: true, 130 | title: "Flexpilot: Select the Google Generative AI model", 131 | }); 132 | if (model === undefined) { 133 | throw new Error("User cancelled model selection"); 134 | } 135 | 136 | // Test the connection credentials 137 | await vscode.window.withProgress( 138 | { 139 | location: vscode.ProgressLocation.Notification, 140 | title: "Flexpilot", 141 | cancellable: false, 142 | }, 143 | async (progress) => { 144 | progress.report({ 145 | message: "Testing connection credentials", 146 | }); 147 | const google = createGoogleGenerativeAI({ 148 | apiKey: apiKey, 149 | baseURL: baseUrl, 150 | }); 151 | await generateText({ 152 | prompt: "Hello", 153 | maxTokens: 3, 154 | model: google.chat(model.label), 155 | }); 156 | logger.info("Connection credentials test successful"); 157 | }, 158 | ); 159 | 160 | // Save the selected model configuration 161 | await storage.models.set(nickname, { 162 | baseUrl: baseUrl, 163 | apiKey: apiKey, 164 | model: model.label, 165 | nickname: nickname, 166 | providerId: GoogleChatModelProvider.providerId, 167 | }); 168 | 169 | logger.info(`Model configuration saved for ${nickname}`); 170 | }; 171 | 172 | /** 173 | * Creates and returns a LanguageModelV1 instance 174 | * @returns {Promise} A promise that resolves to a LanguageModelV1 instance 175 | */ 176 | async model(): Promise { 177 | logger.debug(`Creating LanguageModelV1 instance for ${this.config.model}`); 178 | const google = createGoogleGenerativeAI({ 179 | baseURL: this.config.baseUrl, 180 | apiKey: this.config.apiKey, 181 | }); 182 | return google.chat(this.config.model); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/providers/groq.ts: -------------------------------------------------------------------------------- 1 | import { createOpenAI } from "@ai-sdk/openai"; 2 | import { generateText, LanguageModelV1 } from "ai"; 3 | import OpenAI from "openai"; 4 | import * as vscode from "vscode"; 5 | import { IChatModelProvider, IModelConfig } from "../interfaces"; 6 | import { logger } from "../logger"; 7 | import { storage } from "../storage"; 8 | 9 | /** 10 | * Extended interface for GroqCloud chat model configuration 11 | */ 12 | interface IGroqCloudChatModelConfig extends IModelConfig { 13 | apiKey: string; 14 | baseUrl: string; 15 | } 16 | 17 | /** 18 | * Interface representing a GroqCloud model 19 | * @see https://console.groqcloud.com/docs/api-reference#models 20 | */ 21 | interface IGroqCloudModel { 22 | id: string; 23 | active: boolean; 24 | } 25 | 26 | /** 27 | * Default help prompt for GroqCloud API key configuration 28 | */ 29 | const DEFAULT_HELP_PROMPT = 30 | "Click [here](https://docs.flexpilot.ai/model-providers/groq.html) for more information"; 31 | 32 | /** 33 | * GroqCloud Chat Model Provider class 34 | * Manages the configuration and creation of GroqCloud chat models 35 | */ 36 | export class GroqCloudChatModelProvider extends IChatModelProvider { 37 | static readonly providerName = "GroqCloud"; 38 | static readonly providerId = "groqcloud-chat"; 39 | static readonly providerType = "chat" as const; 40 | public readonly config: IGroqCloudChatModelConfig; 41 | 42 | /** 43 | * Constructor for GroqCloudChatModelProvider 44 | * @param {string} nickname - The nickname for the model configuration 45 | * @throws {Error} If the model configuration is not found 46 | */ 47 | constructor(nickname: string) { 48 | super(nickname); 49 | logger.debug( 50 | `Initializing GroqCloudChatModelProvider with nickname: ${nickname}`, 51 | ); 52 | const config = storage.models.get(nickname); 53 | if (!config) { 54 | throw new Error(`Model configuration not found for ${nickname}`); 55 | } 56 | this.config = config; 57 | logger.info(`GroqCloudChatModelProvider initialized for ${nickname}`); 58 | } 59 | 60 | /** 61 | * Configures a new GroqCloud model 62 | * @param {string} nickname - The nickname for the new model configuration 63 | * @throws {Error} If configuration process is cancelled or fails 64 | */ 65 | static readonly configure = async (nickname: string): Promise => { 66 | logger.info(`Configuring GroqCloud model with nickname: ${nickname}`); 67 | 68 | const config = storage.models.get(nickname); 69 | 70 | // Prompt user for API key 71 | let apiKey = await vscode.window.showInputBox({ 72 | ignoreFocusOut: true, 73 | value: config?.apiKey ?? "", 74 | valueSelection: [0, 0], 75 | placeHolder: "e.g., gsk_vgAcnlKOXdklg2AWLUv...", // cspell:disable-line 76 | prompt: DEFAULT_HELP_PROMPT, 77 | title: "Flexpilot: Enter your GroqCloud API key", 78 | }); 79 | if (apiKey === undefined) { 80 | throw new Error("User cancelled API key input"); 81 | } 82 | apiKey = apiKey.trim(); 83 | 84 | // Prompt user for base URL 85 | const defaultBaseUrl = "https://api.groq.com/openai/v1"; 86 | let baseUrl = await vscode.window.showInputBox({ 87 | ignoreFocusOut: true, 88 | value: config?.baseUrl ?? defaultBaseUrl, 89 | valueSelection: [0, 0], 90 | placeHolder: `e.g., ${defaultBaseUrl}`, 91 | prompt: DEFAULT_HELP_PROMPT, 92 | title: "Flexpilot: Enter the base URL for GroqCloud API", 93 | }); 94 | if (baseUrl === undefined) { 95 | throw new Error("User cancelled base URL input"); 96 | } 97 | baseUrl = baseUrl.trim(); 98 | 99 | // Fetch available models from GroqCloud API 100 | const response = await vscode.window.withProgress( 101 | { 102 | location: vscode.ProgressLocation.Notification, 103 | title: "Flexpilot", 104 | cancellable: true, 105 | }, 106 | async (progress) => { 107 | progress.report({ 108 | message: "Fetching available models", 109 | }); 110 | const openai = new OpenAI({ 111 | apiKey: apiKey, 112 | baseURL: baseUrl, 113 | }); 114 | logger.debug("Fetching models from GroqCloud API"); 115 | const models = await openai.models.list(); 116 | logger.debug(`Fetched ${models.data.length} models`); 117 | return models.data as unknown as IGroqCloudModel[]; 118 | }, 119 | ); 120 | 121 | // Filter out models that are not supported for content generation 122 | const modelPickUpItems: vscode.QuickPickItem[] = []; 123 | for (const model of response) { 124 | if (model.active) { 125 | modelPickUpItems.push({ label: model.id }); 126 | } 127 | } 128 | 129 | // Prompt user to select a model 130 | const model = await vscode.window.showQuickPick(modelPickUpItems, { 131 | placeHolder: "Select a chat model", 132 | ignoreFocusOut: true, 133 | title: "Flexpilot: Select the GroqCloud model", 134 | }); 135 | if (model === undefined) { 136 | throw new Error("User cancelled model selection"); 137 | } 138 | 139 | // Test the connection credentials 140 | await vscode.window.withProgress( 141 | { 142 | location: vscode.ProgressLocation.Notification, 143 | title: "Flexpilot", 144 | cancellable: false, 145 | }, 146 | async (progress) => { 147 | progress.report({ 148 | message: "Testing connection credentials", 149 | }); 150 | const groqcloud = createOpenAI({ 151 | apiKey: apiKey, 152 | baseURL: baseUrl, 153 | }); 154 | logger.debug("Testing connection with a simple prompt"); 155 | await generateText({ 156 | prompt: "Hello", 157 | maxTokens: 3, 158 | model: groqcloud.chat(model.label), 159 | }); 160 | logger.info("Connection credentials test successful"); 161 | }, 162 | ); 163 | 164 | // Save the selected model configuration 165 | logger.debug(`Saving model configuration for ${nickname}`); 166 | await storage.models.set(nickname, { 167 | baseUrl: baseUrl, 168 | apiKey: apiKey, 169 | model: model.label, 170 | nickname: nickname, 171 | providerId: GroqCloudChatModelProvider.providerId, 172 | }); 173 | 174 | logger.info(`Model configuration saved for ${nickname}`); 175 | }; 176 | 177 | /** 178 | * Creates and returns a LanguageModelV1 instance 179 | * @returns {Promise} A promise that resolves to a LanguageModelV1 instance 180 | */ 181 | async model(): Promise { 182 | logger.debug(`Creating LanguageModelV1 instance for ${this.config.model}`); 183 | const groqcloud = createOpenAI({ 184 | baseURL: this.config.baseUrl, 185 | apiKey: this.config.apiKey, 186 | }); 187 | logger.info(`LanguageModelV1 instance created for ${this.config.model}`); 188 | return groqcloud.chat(this.config.model); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { LOCATIONS } from "../constants"; 2 | import { events } from "../events"; 3 | import { ILocationName, IModelType } from "../interfaces"; 4 | import { logger } from "../logger"; 5 | import { storage } from "../storage"; 6 | import { AnthropicChatModelProvider } from "./anthropic"; 7 | import { 8 | AzureOpenAIChatModelProvider, 9 | AzureOpenAICompletionModelProvider, 10 | } from "./azure"; 11 | import { GenericChatModelProvider } from "./generic"; 12 | import { GoogleChatModelProvider } from "./google"; 13 | import { GroqCloudChatModelProvider } from "./groq"; 14 | import { 15 | MistralAIChatModelProvider, 16 | MistralAICompletionModelProvider, 17 | } from "./mistral-ai"; 18 | import { 19 | OpenAIChatModelProvider, 20 | OpenAICompletionModelProvider, 21 | } from "./openai"; 22 | 23 | /** 24 | * Array of available model provider classes. 25 | */ 26 | export const ModelProviders = [ 27 | OpenAIChatModelProvider, 28 | GoogleChatModelProvider, 29 | MistralAIChatModelProvider, 30 | AzureOpenAIChatModelProvider, 31 | AnthropicChatModelProvider, 32 | GroqCloudChatModelProvider, 33 | GenericChatModelProvider, 34 | OpenAICompletionModelProvider, 35 | MistralAICompletionModelProvider, 36 | AzureOpenAICompletionModelProvider, 37 | ] as const; 38 | 39 | /** 40 | * Interfaces for the model provider class. 41 | */ 42 | export type IModelProvider = InstanceType< 43 | Extract<(typeof ModelProviders)[number], { providerType: T }> 44 | >; 45 | 46 | export class Test123 {} 47 | 48 | /** 49 | * Manages model providers for different locations. 50 | * Implements the Singleton pattern to ensure a single instance across the application. 51 | */ 52 | export class ModelProviderManager { 53 | private readonly modelProviders: Map = 54 | new Map(); 55 | private static instance: ModelProviderManager; 56 | 57 | private constructor(extensionContext = storage.getContext()) { 58 | // Check for changes in the lastProviderUpdatedAt secret to re-initialize providers 59 | extensionContext.subscriptions.push( 60 | events.onFire(async (event) => { 61 | logger.debug(`Event Action Started: ${event.name}`); 62 | if (event.name === "modelProvidersUpdated") { 63 | logger.info("Re-initializing model providers"); 64 | this.updateProviders(); 65 | } 66 | }), 67 | ); 68 | logger.info("ModelProviderManager instance created"); 69 | } 70 | 71 | /** 72 | * Gets the singleton instance of ModelProviderManager. 73 | * @returns The ModelProviderManager instance 74 | */ 75 | public static getInstance(): ModelProviderManager { 76 | if (!ModelProviderManager.instance) { 77 | ModelProviderManager.instance = new ModelProviderManager(); 78 | logger.debug("New ModelProviderManager instance created"); 79 | } 80 | return ModelProviderManager.instance; 81 | } 82 | 83 | /** 84 | * Gets the provider for a specific location. 85 | * @param locationName - The name of the location 86 | * @returns The model provider for the specified location, or undefined if not found 87 | */ 88 | public getProvider( 89 | locationName: ILocationName, 90 | ): IModelProvider | undefined { 91 | const provider = this.modelProviders.get(locationName); 92 | if (!provider) { 93 | logger.info(`No provider found for location ${locationName}`); 94 | return undefined; 95 | } 96 | logger.debug(`Provider retrieved for location: ${locationName}`); 97 | return provider as IModelProvider; 98 | } 99 | 100 | /** 101 | * Updates the provider for all locations based on the usage preference. 102 | */ 103 | public async updateProviders(): Promise { 104 | logger.info("Initializing providers for all locations"); 105 | await Promise.all( 106 | LOCATIONS.map(async (location) => { 107 | try { 108 | logger.info(`Updating provider for location: ${location.name}`); 109 | 110 | // Trigger once before updating provider 111 | if (location.name === "Inline Completion") { 112 | events.fire({ 113 | name: "inlineCompletionProviderUpdated", 114 | payload: { updatedAt: Date.now() }, 115 | }); 116 | } 117 | 118 | // Delete the existing provider 119 | this.modelProviders.delete(location.name); 120 | 121 | // Get usage preference for the location 122 | const usagePreference = storage.usage.get(location.name); 123 | 124 | if (!usagePreference) { 125 | return logger.info( 126 | `No usage preference found for location: ${location.name}`, 127 | ); 128 | } 129 | 130 | // Find the appropriate ModelProvider based on the usage preference 131 | const ModelProvider = ModelProviders.find( 132 | (item) => item.providerId === usagePreference.providerId, 133 | ); 134 | if (!ModelProvider) { 135 | return logger.info( 136 | `No matching ModelProvider found for providerId: ${usagePreference.providerId}`, 137 | ); 138 | } 139 | 140 | // Create and set the new provider 141 | const modelProvider = new ModelProvider(usagePreference.nickname); 142 | await modelProvider.initialize(); 143 | this.modelProviders.set(location.name, modelProvider); 144 | logger.info( 145 | `Provider updated successfully for location: ${location.name}`, 146 | ); 147 | 148 | // Trigger once after updating provider 149 | if (location.name === "Inline Completion") { 150 | events.fire({ 151 | name: "inlineCompletionProviderUpdated", 152 | payload: { updatedAt: Date.now() }, 153 | }); 154 | } 155 | } catch (error) { 156 | logger.error(error as Error); 157 | logger.notifyError("Error updating model provider configuration"); 158 | } 159 | }), 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/session.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import * as vscode from "vscode"; 3 | import packageJson from "../package.json"; 4 | import InlineCompletionProvider from "./completion"; 5 | import InlineChatParticipant from "./inline-chat"; 6 | import { logger } from "./logger"; 7 | import PanelChatParticipant from "./panel-chat"; 8 | import { ModelProviderManager } from "./providers"; 9 | import { storage } from "./storage"; 10 | import { setContext } from "./utilities"; 11 | 12 | /** 13 | * SessionManager class to handle VSCode extension session management and related events. 14 | * This class follows the Singleton pattern to ensure only one instance exists. 15 | */ 16 | export class SessionManager extends vscode.Disposable { 17 | private readonly disposables: vscode.Disposable[] = []; 18 | private static instance: SessionManager; 19 | 20 | private constructor( 21 | private readonly extensionContext = storage.getContext(), 22 | ) { 23 | // Call the parent constructor 24 | super(() => { 25 | this.disposables.forEach((disposable) => disposable.dispose()); 26 | this.disposeSessionFeatures(); 27 | logger.info("Session manager disposed"); 28 | }); 29 | 30 | // Register the session manager 31 | extensionContext.subscriptions.push(this); 32 | 33 | // Handle the session change 34 | this.disposables.push( 35 | vscode.authentication.onDidChangeSessions(() => 36 | this.handleSessionChange(), 37 | ), 38 | ); 39 | 40 | // Initialize the session manager 41 | logger.info("Session manager initialized"); 42 | } 43 | 44 | /** 45 | * Gets the singleton instance of SessionManager. 46 | */ 47 | public static async register() { 48 | if (!SessionManager.instance) { 49 | SessionManager.instance = new SessionManager(); 50 | SessionManager.instance.handleSessionChange(); 51 | logger.debug("New SessionManager instance created"); 52 | } 53 | } 54 | 55 | private async checkNewlyInstalled() { 56 | const globalStorageUri = this.extensionContext.globalStorageUri; 57 | const flagUri = vscode.Uri.joinPath(globalStorageUri, "installed_at"); 58 | 59 | // Check if the globalStorageUri folder exists in storage 60 | try { 61 | await vscode.workspace.fs.stat(globalStorageUri); 62 | } catch (error) { 63 | logger.warn(`Folder not found at: ${globalStorageUri}`); 64 | logger.warn(String(error)); 65 | vscode.workspace.fs.createDirectory(globalStorageUri); 66 | } 67 | 68 | // Check if the flag URI file exists in storage 69 | try { 70 | await vscode.workspace.fs.stat(flagUri); 71 | logger.debug(`Extension already installed, restoring state`); 72 | } catch (error) { 73 | logger.warn(String(error)); 74 | logger.debug("Extension newly installed, clearing state"); 75 | await this.clearGlobalState(); 76 | await vscode.workspace.fs.writeFile(flagUri, new Uint8Array(0)); 77 | } 78 | } 79 | 80 | /** 81 | * Handles changes in authentication sessions. 82 | */ 83 | public async handleSessionChange(): Promise { 84 | const session = await vscode.authentication.getSession( 85 | "github", 86 | ["public_repo", "user:email"], 87 | { createIfNone: false }, 88 | ); 89 | await this.checkNewlyInstalled(); 90 | storage.session.set(session); 91 | setContext("isLoggedIn", !!session); 92 | if (session) { 93 | this.handleActiveSession(); 94 | } else { 95 | this.handleNoSession(); 96 | } 97 | logger.debug("Session change handled successfully"); 98 | } 99 | 100 | /** 101 | * Handles the case when there's no active session. 102 | */ 103 | private handleNoSession(): void { 104 | this.disposeSessionFeatures(); 105 | this.clearGlobalState(); 106 | this.showSignInPrompt(); 107 | logger.info("No active session, extension deactivated"); 108 | } 109 | 110 | /** 111 | * Handles the case when there's an active session. 112 | */ 113 | private async handleActiveSession(): Promise { 114 | logger.info("GitHub session established"); 115 | if (await storage.get("github.support")) { 116 | this.starGitHubRepository(); 117 | } else { 118 | this.showGithubSupportNotification(); 119 | } 120 | this.registerSessionFeatures(); 121 | await ModelProviderManager.getInstance().updateProviders(); 122 | } 123 | 124 | /** 125 | * Clears the global state. 126 | */ 127 | private async clearGlobalState(): Promise { 128 | const keys = this.extensionContext.globalState.keys(); 129 | for (const key of keys) { 130 | await this.extensionContext.globalState.update(key, undefined); 131 | } 132 | logger.debug("Global state cleared"); 133 | } 134 | 135 | /** 136 | * Shows a sign-in prompt to the user. 137 | */ 138 | private showSignInPrompt(): void { 139 | vscode.window 140 | .showInformationMessage( 141 | "Please sign in to your GitHub account to start using Flexpilot", 142 | "Sign in to Chat", 143 | ) 144 | .then((selection) => { 145 | if (selection === "Sign in to Chat") { 146 | vscode.commands.executeCommand("flexpilot.github.signin"); 147 | } 148 | }); 149 | } 150 | 151 | /** 152 | * Displays a notification to support the project on GitHub. 153 | */ 154 | private showGithubSupportNotification(): void { 155 | vscode.window 156 | .showInformationMessage( 157 | "Support our mission to make AI accessible - give us a GitHub star by just clicking `Yes` button!", 158 | "Yes (recommended)", 159 | "No, I don't like to support", 160 | ) 161 | .then(async (support) => { 162 | if (support === "Yes (recommended)") { 163 | storage.set("github.support", true); 164 | this.starGitHubRepository().then(() => this.showSponsorPrompt()); 165 | } 166 | }); 167 | } 168 | 169 | /** 170 | * Shows a prompt to become a sponsor. 171 | */ 172 | private showSponsorPrompt(): void { 173 | vscode.window 174 | .showInformationMessage( 175 | "Thanks for helping us make AI open and accessible to everyone!", 176 | "Become a Sponsor", 177 | ) 178 | .then(async (sponsor) => { 179 | if (sponsor === "Become a Sponsor") { 180 | vscode.env.openExternal(vscode.Uri.parse(packageJson.repository.url)); 181 | } 182 | }); 183 | } 184 | 185 | /** 186 | * Stars the GitHub repository to support the project. 187 | */ 188 | private async starGitHubRepository(): Promise { 189 | try { 190 | const githubSession = storage.session.get(); 191 | if (!githubSession) { 192 | return; 193 | } 194 | await axios.put( 195 | "https://api.github.com/user/starred/flexpilot-ai/vscode-extension", 196 | null, 197 | { 198 | headers: { 199 | Accept: "application/vnd.github+json", 200 | "X-GitHub-Api-Version": "2022-11-28", 201 | Authorization: `Bearer ${githubSession.accessToken}`, 202 | }, 203 | }, 204 | ); 205 | } catch (error) { 206 | logger.warn(`Unable to star the GitHub repository: ${error}`); 207 | } 208 | } 209 | 210 | /** 211 | * Disposes of the current session features. 212 | */ 213 | private disposeSessionFeatures(): void { 214 | PanelChatParticipant.dispose(); 215 | InlineCompletionProvider.dispose(); 216 | InlineChatParticipant.dispose(); 217 | logger.debug("Session features disposed"); 218 | } 219 | 220 | /** 221 | * Registers the features for the current session. 222 | */ 223 | private registerSessionFeatures(): void { 224 | try { 225 | PanelChatParticipant.register(); 226 | InlineCompletionProvider.register(); 227 | InlineChatParticipant.register(); 228 | logger.info("Session features registered"); 229 | } catch (error) { 230 | logger.error(error as Error); 231 | logger.notifyError(`Failed to register session features: ${error}`); 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/startup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommentJSONValue, 3 | parse, 4 | stringify, 5 | type CommentObject, 6 | } from "comment-json"; 7 | import * as path from "path"; 8 | import * as vscode from "vscode"; 9 | import { IPackageJson } from "./interfaces"; 10 | import { logger } from "./logger"; 11 | import { storage } from "./storage"; 12 | import { isFileExists } from "./utilities"; 13 | 14 | /** 15 | * Show notification to restart VS Code to apply changes 16 | */ 17 | const triggerVscodeRestart = async () => { 18 | // Get the current value of the titleBarStyle setting 19 | const existingValue = vscode.workspace 20 | .getConfiguration("window") 21 | .get("titleBarStyle"); 22 | 23 | // Toggle the value of the titleBarStyle setting 24 | await vscode.workspace 25 | .getConfiguration("window") 26 | .update( 27 | "titleBarStyle", 28 | existingValue === "native" ? "custom" : "native", 29 | vscode.ConfigurationTarget.Global, 30 | ); 31 | 32 | // Sleep for few milliseconds 33 | await new Promise((resolve) => setTimeout(resolve, 200)); 34 | 35 | // Toggle the value back to the original value 36 | await vscode.workspace 37 | .getConfiguration("window") 38 | .update( 39 | "titleBarStyle", 40 | existingValue === "native" ? "native" : "custom", 41 | vscode.ConfigurationTarget.Global, 42 | ); 43 | }; 44 | 45 | /** 46 | * Checks if the proposed API is disabled in the current environment. 47 | */ 48 | const isProposedApiDisabled = async () => { 49 | try { 50 | await vscode.lm.fileIsIgnored(vscode.Uri.file("/"), { 51 | isCancellationRequested: false, 52 | onCancellationRequested: () => new vscode.EventEmitter(), 53 | }); 54 | return false; 55 | } catch (error) { 56 | logger.error(error as Error); 57 | logger.error("Proposed API disabled for Flexpilot"); 58 | return true; 59 | } 60 | }; 61 | 62 | /** 63 | * Checks if GitHub Copilot is active in the current environment. 64 | */ 65 | const isGitHubCopilotActive = () => { 66 | // Get the extension by its identifier 67 | const extension = vscode.extensions.getExtension("GitHub.copilot"); 68 | 69 | // Check if the extension is installed 70 | if (extension) { 71 | logger.info("GitHub Copilot is installed"); 72 | return true; 73 | } else { 74 | logger.info("GitHub Copilot is not installed."); 75 | return false; 76 | } 77 | }; 78 | 79 | /** 80 | * Checks if the package.json file is outdated and updates it. 81 | */ 82 | const isPackageJsonOutdated = async () => { 83 | // Get the path of the package.json file 84 | const packageJsonUri = vscode.Uri.joinPath( 85 | storage.getContext().extensionUri, 86 | "package.json", 87 | ); 88 | logger.debug(`Package JSON Path: ${packageJsonUri}`); 89 | 90 | // Get the loaded package.json content 91 | const loadedPackageJson = storage.getContext().extension.packageJSON; 92 | 93 | // Check if the package.json file is outdated 94 | let isPackageJsonOutdated = false; 95 | 96 | // Parse the package.json content 97 | const packageJsonBuffer = await vscode.workspace.fs.readFile(packageJsonUri); 98 | const packageJson: IPackageJson = JSON.parse( 99 | Buffer.from(packageJsonBuffer).toString("utf8"), 100 | ); 101 | 102 | if (loadedPackageJson.contributes.menus["scm/inputBox"] === undefined) { 103 | // Add the "scm/inputBox" menu to the "contributes" section 104 | packageJson.contributes.menus["scm/inputBox"] = [ 105 | { 106 | when: "scmProvider == git", 107 | command: "flexpilot.git.generateCommitMessage", 108 | }, 109 | ]; 110 | 111 | // Set flag to update the package.json file 112 | isPackageJsonOutdated = true; 113 | logger.info("Successfully updated package with 'scm/inputBox'"); 114 | } 115 | 116 | if (loadedPackageJson.enabledApiProposals === undefined) { 117 | // Add the proposed API to the "contributes" section 118 | packageJson.enabledApiProposals = packageJson.enabledApiProposalsOriginal; 119 | 120 | // Set flag to update the package.json file 121 | isPackageJsonOutdated = true; 122 | logger.info("Successfully updated package with 'enabledApiProposals'"); 123 | } 124 | 125 | if (loadedPackageJson.contributes.languageModels === undefined) { 126 | // Add the "languageModels" section to the "contributes" section 127 | packageJson.contributes.languageModels = { 128 | vendor: "flexpilot", 129 | }; 130 | 131 | // Set flag to update the package.json file 132 | isPackageJsonOutdated = true; 133 | logger.info("Successfully updated package with 'scm/inputBox'"); 134 | } 135 | 136 | if (isPackageJsonOutdated) { 137 | // Write the changes to the package.json file 138 | await vscode.workspace.fs.writeFile( 139 | packageJsonUri, 140 | Buffer.from(JSON.stringify(packageJson, null, 2), "utf8"), 141 | ); 142 | } 143 | 144 | // Return the flag indicating if the package.json file was updated 145 | return isPackageJsonOutdated; 146 | }; 147 | 148 | /** 149 | * Checks if the argv.json file is outdated and updates it. 150 | */ 151 | const isArgvJsonOutdated = async () => { 152 | // Get the path of the argv.json file from storage 153 | let argvPath = storage.get("argv.path"); 154 | let argvJsonOutdated = false; 155 | 156 | // Check if the argv.json file is outdated and trigger the update 157 | if (argvPath && !isFileExists(vscode.Uri.parse(argvPath))) { 158 | argvPath = undefined; 159 | } 160 | 161 | if (!argvPath) { 162 | // Open runtime arguments configuration 163 | await vscode.commands.executeCommand( 164 | "workbench.action.configureRuntimeArguments", 165 | ); 166 | 167 | // Find the argv.json file in visible text editors 168 | const argvDocument = vscode.window.visibleTextEditors 169 | .map((item) => item.document) 170 | .filter((item) => path.basename(item.uri.fsPath) === "argv.json") 171 | .pop(); 172 | 173 | // throw an error if the file is not found 174 | if (!argvDocument) { 175 | throw new Error("argv.json file not found in visible editors"); 176 | } 177 | 178 | // Set the argv path in storage 179 | argvPath = argvDocument.uri.toString(); 180 | await storage.set("argv.path", argvPath); 181 | 182 | // Make the argv file active and close it 183 | await vscode.window.showTextDocument(argvDocument); 184 | await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); 185 | } 186 | 187 | // Create the URI for the argv.json file 188 | const argvFileUri = vscode.Uri.parse(argvPath); 189 | 190 | // Get the extension ID 191 | const extensionId = storage.getContext().extension.id; 192 | logger.debug(`Extension ID: ${extensionId}`); 193 | 194 | // Parse the argv.json content 195 | const buffer = await vscode.workspace.fs.readFile(argvFileUri); 196 | const argvJson = parse(Buffer.from(buffer).toString("utf8")) as CommentObject; 197 | 198 | // Add the extension ID to the "enable-proposed-api" array 199 | if (Array.isArray(argvJson["enable-proposed-api"])) { 200 | if (!argvJson["enable-proposed-api"].includes(extensionId)) { 201 | argvJsonOutdated = true; 202 | argvJson["enable-proposed-api"].push(extensionId); 203 | logger.debug(`Added ${extensionId} to existing array`); 204 | } else { 205 | logger.debug(`${extensionId} already in array`); 206 | } 207 | } else if (typeof argvJson["enable-proposed-api"] === "string") { 208 | argvJsonOutdated = true; 209 | argvJson["enable-proposed-api"] = [ 210 | argvJson["enable-proposed-api"], 211 | extensionId, 212 | ] as CommentJSONValue; 213 | logger.debug(`Created new array with ${extensionId}`); 214 | } else { 215 | argvJsonOutdated = true; 216 | argvJson["enable-proposed-api"] = [extensionId] as CommentJSONValue; 217 | logger.debug(`Created new array with ${extensionId}`); 218 | } 219 | 220 | // Add the extension ID to the "log-level" array 221 | const logLevel = `${extensionId}=debug`; 222 | if (Array.isArray(argvJson["log-level"])) { 223 | if (!argvJson["log-level"].includes(logLevel)) { 224 | argvJsonOutdated = true; 225 | argvJson["log-level"].push(logLevel); 226 | logger.debug(`Added ${logLevel} to existing array`); 227 | } else { 228 | logger.debug(`${logLevel} already in array`); 229 | } 230 | } else if (typeof argvJson["log-level"] === "string") { 231 | argvJsonOutdated = true; 232 | argvJson["log-level"] = [ 233 | argvJson["log-level"], 234 | logLevel, 235 | ] as CommentJSONValue; 236 | logger.debug(`Created new array with ${logLevel}`); 237 | } else { 238 | argvJsonOutdated = true; 239 | argvJson["log-level"] = [logLevel] as CommentJSONValue; 240 | logger.debug(`Created new array with ${logLevel}`); 241 | } 242 | 243 | // If the argv.json file was updated, write the changes 244 | if (argvJsonOutdated) { 245 | await vscode.workspace.fs.writeFile( 246 | argvFileUri, 247 | Buffer.from(stringify(argvJson, null, 4), "utf8"), 248 | ); 249 | logger.info("Successfully updated argv.json"); 250 | } 251 | 252 | // Return the flag indicating if the argv.json file was updated 253 | return argvJsonOutdated; 254 | }; 255 | 256 | /** 257 | * updates the runtime arguments configuration to enable proposed API, log level, etc ... 258 | */ 259 | export const updateRuntimeArguments = async () => { 260 | // Initialize the flag to require a restart 261 | let requireRestart = false; 262 | let restartMessage = 263 | "Flexpilot: Please restart VS Code to apply the latest updates"; 264 | 265 | // Update `chat.commandCenter.enabled` to always true 266 | vscode.workspace.onDidChangeConfiguration(() => { 267 | vscode.workspace 268 | .getConfiguration("chat.commandCenter") 269 | .update("enabled", true, vscode.ConfigurationTarget.Global); 270 | }); 271 | 272 | // Check if the argv.json file is outdated 273 | if (await isArgvJsonOutdated()) { 274 | logger.warn("Updated runtime arguments, restart required"); 275 | requireRestart = true; 276 | } 277 | 278 | // Check if the package.json file is outdated 279 | if (await isPackageJsonOutdated()) { 280 | logger.warn("Updated package.json, restart required"); 281 | requireRestart = true; 282 | } 283 | 284 | // Check if the proposed API is disabled 285 | if (await isProposedApiDisabled()) { 286 | logger.warn("Proposed API is disabled, restart required"); 287 | requireRestart = true; 288 | } 289 | 290 | // Check if GitHub Copilot is active 291 | if (isGitHubCopilotActive()) { 292 | logger.warn("GitHub Copilot is active, restart required"); 293 | restartMessage = 294 | "Flexpilot: To ensure Flexpilot functions correctly, kindly disable `GitHub Copilot` and Restart"; 295 | } 296 | 297 | // Notify the user about the required restart 298 | if (requireRestart) { 299 | // Show a notification to restart VS Code 300 | vscode.window 301 | .showInformationMessage(restartMessage, "Restart", "View Logs") 302 | .then((selection) => { 303 | if (selection === "Restart") { 304 | triggerVscodeRestart(); 305 | } else if (selection === "View Logs") { 306 | logger.showOutputChannel(); 307 | } 308 | }); 309 | 310 | // Throw an error to stop the execution 311 | throw new Error("Flexpilot: VS Code restart required"); 312 | } 313 | 314 | // Log the successful activation 315 | logger.info("Successfully updated runtime arguments"); 316 | }; 317 | -------------------------------------------------------------------------------- /src/status-icon.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { events } from "./events"; 3 | import { logger } from "./logger"; 4 | import { ModelProviderManager } from "./providers"; 5 | import { storage } from "./storage"; 6 | 7 | /** 8 | * Status icon manager to handle the status icon and state. 9 | * Created as a singleton to ensure a single instance across the application. 10 | */ 11 | class StatusIconManager { 12 | private static instance: StatusIconManager; 13 | private readonly statusBarItem: vscode.StatusBarItem; 14 | public state: "enabled" | "disabled" = "enabled"; 15 | 16 | private constructor(extensionContext = storage.getContext()) { 17 | // Create the status bar item 18 | logger.info("StatusIconManager instance created"); 19 | this.statusBarItem = vscode.window.createStatusBarItem( 20 | vscode.StatusBarAlignment.Right, 21 | ); 22 | 23 | extensionContext.subscriptions.push(this.statusBarItem); 24 | 25 | // Update the status bar icon when the inline completion provider is updated 26 | extensionContext.subscriptions.push( 27 | events.onFire((event) => { 28 | if (event.name === "inlineCompletionProviderUpdated") { 29 | this.updateStatusBarIcon(); 30 | } 31 | }), 32 | ); 33 | 34 | // Initialize the status bar item 35 | this.initializeStatusBar(); 36 | this.updateStatusBarIcon(); 37 | 38 | // Update the status bar icon when the active text editor changes 39 | vscode.window.onDidChangeActiveTextEditor(() => this.updateStatusBarIcon()); 40 | } 41 | 42 | /** 43 | * Get the singleton instance of StatusIconManager. 44 | * @returns {StatusIconManager} The singleton instance. 45 | */ 46 | public static getInstance(): StatusIconManager { 47 | if (!StatusIconManager.instance) { 48 | // Create a new instance if not already created 49 | StatusIconManager.instance = new StatusIconManager(); 50 | logger.debug("New StatusIconManager instance created"); 51 | } 52 | return StatusIconManager.instance; 53 | } 54 | 55 | /** 56 | * Initialize the status bar item with default properties. 57 | */ 58 | private initializeStatusBar(): void { 59 | logger.debug("Initializing status bar"); 60 | this.statusBarItem.accessibilityInformation = { 61 | label: "Flexpilot Status", 62 | }; 63 | this.statusBarItem.tooltip = "Flexpilot Status"; 64 | this.statusBarItem.name = "Flexpilot Status"; 65 | this.statusBarItem.command = "flexpilot.status.icon.menu"; 66 | this.statusBarItem.show(); 67 | logger.debug("Status bar initialized"); 68 | } 69 | 70 | /** 71 | * Update the status bar icon based on the current state. 72 | */ 73 | private updateStatusBarIcon(): void { 74 | let isCompletionsEnabled = true; 75 | 76 | // Check if the completions are enabled 77 | logger.debug("Updating status bar icon"); 78 | const editor = vscode.window.activeTextEditor; 79 | if (!editor) { 80 | logger.debug("No active editor, setting status to disabled"); 81 | isCompletionsEnabled = false; 82 | } 83 | 84 | // Check if the inline completion provider is available 85 | const provider = 86 | ModelProviderManager.getInstance().getProvider("Inline Completion"); 87 | if (!provider) { 88 | logger.debug("No provider found, setting status to disabled"); 89 | isCompletionsEnabled = false; 90 | } 91 | 92 | // Check if the language is disabled 93 | const config = storage.get("completions.disabled.languages") || []; 94 | if (editor && config.includes(editor.document.languageId)) { 95 | logger.debug("Language disabled, setting status to disabled"); 96 | isCompletionsEnabled = false; 97 | } 98 | logger.debug("Setting status to enabled"); 99 | 100 | // Update the status bar icon 101 | if (isCompletionsEnabled) { 102 | this.state = "enabled"; 103 | this.statusBarItem.text = "$(flexpilot-default)"; 104 | } else { 105 | this.state = "disabled"; 106 | this.statusBarItem.text = "$(flexpilot-disabled)"; 107 | } 108 | } 109 | 110 | /** 111 | * Reset the status bar item to default state. 112 | */ 113 | public reset(): void { 114 | logger.debug("Resetting status bar"); 115 | if (this.state === "disabled") { 116 | this.statusBarItem.text = "$(flexpilot-disabled)"; 117 | } else { 118 | this.statusBarItem.text = "$(flexpilot-default)"; 119 | } 120 | } 121 | 122 | /** 123 | * Set the status bar item to loading state. 124 | */ 125 | public setLoading(): void { 126 | logger.debug("Setting status bar to loading"); 127 | this.statusBarItem.text = "$(loading~spin)"; 128 | } 129 | } 130 | 131 | // Export the StatusIconManager instance 132 | export const statusIcon = StatusIconManager.getInstance(); 133 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import packageJson from "../package.json"; 3 | import { ILocationName, IModelConfig, IUsagePreference } from "./interfaces"; 4 | import { logger } from "./logger"; 5 | 6 | /** 7 | * IKeyValueStore interface to define the structure of permanent key-value store. 8 | */ 9 | interface IKeyValueStore { 10 | "github.support": boolean; 11 | "completions.disabled.languages": string[]; 12 | "argv.path": string; 13 | } 14 | 15 | /** 16 | * IWorkspaceConfigKeys type to define the keys of workspace configuration. 17 | */ 18 | type IWorkspaceConfigKeys = 19 | keyof (typeof packageJson)["contributes"]["configuration"]["properties"]; 20 | 21 | /** 22 | * StorageManager class provides a centralized state management mechanism for the extension. 23 | * It implements the Singleton pattern to ensure a single instance of the state across the application. 24 | */ 25 | class StorageManager { 26 | private githubSession: vscode.AuthenticationSession | undefined; 27 | private context: vscode.ExtensionContext | undefined; 28 | 29 | /** 30 | * Creates a new instance of StorageManager. 31 | * @param {vscode.ExtensionContext} extensionContext - The extension context provided by VS Code 32 | */ 33 | constructor() { 34 | logger.info("StorageManager instance created"); 35 | } 36 | 37 | /** 38 | * Sets the extension context for the storage manager. 39 | * @param {vscode.ExtensionContext} extensionContext - The extension context provided by VS Code 40 | */ 41 | public setContext(extensionContext: vscode.ExtensionContext) { 42 | this.context = extensionContext; 43 | } 44 | 45 | /** 46 | * Retrieves the extension context 47 | * @returns {vscode.ExtensionContext} The extension context provided by VS Code or undefined 48 | */ 49 | public getContext(): vscode.ExtensionContext { 50 | if (!this.context) { 51 | throw new Error("Storage manager not initialized"); 52 | } 53 | return this.context; 54 | } 55 | 56 | /** 57 | * Retrieves a value from permanent storage. 58 | */ 59 | public get = ( 60 | key: T1, 61 | ): IKeyValueStore[T1] | undefined => { 62 | if (!this.context) { 63 | throw new Error("Storage manager not initialized"); 64 | } 65 | logger.debug(`Getting global state for key: ${key}`); 66 | return this.context.globalState.get(key); 67 | }; 68 | 69 | /** 70 | * Sets a value in permanent storage. 71 | */ 72 | public set = async ( 73 | key: T1, 74 | value: IKeyValueStore[T1] | undefined, 75 | ): Promise => { 76 | if (!this.context) { 77 | throw new Error("Storage manager not initialized"); 78 | } 79 | logger.debug(`Setting global state for key: ${key}`); 80 | return this.context.globalState.update(key, value); 81 | }; 82 | 83 | public session = { 84 | /** 85 | * Retrieves the GitHub session from the storage. 86 | */ 87 | get: (): vscode.AuthenticationSession => { 88 | if (!this.githubSession) { 89 | throw new Error("GitHub session not found"); 90 | } 91 | return this.githubSession; 92 | }, 93 | 94 | /** 95 | * Sets the GitHub session. 96 | */ 97 | set: (session: vscode.AuthenticationSession | undefined) => { 98 | this.githubSession = session; 99 | }, 100 | }; 101 | 102 | public models = { 103 | /** 104 | * Retrieves a value from model providers. 105 | */ 106 | get: (key: string): T | undefined => { 107 | if (!this.context) { 108 | throw new Error("Storage manager not initialized"); 109 | } 110 | const storageKey = `model.providers.${key}`; 111 | logger.debug(`Getting model config for key: ${storageKey}`); 112 | return this.context.globalState.get(storageKey); 113 | }, 114 | 115 | /** 116 | * Sets a value in model providers. 117 | */ 118 | set: async (key: string, value?: T) => { 119 | if (!this.context) { 120 | throw new Error("Storage manager not initialized"); 121 | } 122 | const storageKey = `model.providers.${key}`; 123 | logger.debug(`Setting model config for key: ${storageKey}`); 124 | return this.context.globalState.update(storageKey, value); 125 | }, 126 | 127 | /** 128 | * Lists all the model providers. 129 | */ 130 | list: (): string[] => { 131 | if (!this.context) { 132 | throw new Error("Storage manager not initialized"); 133 | } 134 | logger.debug(`Listing global state for key: model.providers`); 135 | return this.context.globalState 136 | .keys() 137 | .filter((item) => item.startsWith("model.providers.")) 138 | .map((item) => item.replace("model.providers.", "")); 139 | }, 140 | }; 141 | 142 | public usage = { 143 | /** 144 | * Retrieves a value from usage preferences. 145 | */ 146 | get: (key: ILocationName): IUsagePreference | undefined => { 147 | if (!this.context) { 148 | throw new Error("Storage manager not initialized"); 149 | } 150 | const storageKey = `usage.preferences.${key}`; 151 | logger.debug(`Getting usage preference for key: ${storageKey}`); 152 | return this.context.globalState.get(storageKey); 153 | }, 154 | 155 | /** 156 | * Sets a value in usage preferences. 157 | */ 158 | set: async ( 159 | key: ILocationName, 160 | value: T | undefined, 161 | ) => { 162 | if (!this.context) { 163 | throw new Error("Storage manager not initialized"); 164 | } 165 | const storageKey = `usage.preferences.${key}`; 166 | logger.debug(`Setting usage preference for key: ${storageKey}`); 167 | return this.context.globalState.update(storageKey, value); 168 | }, 169 | }; 170 | 171 | public workspace = { 172 | /** 173 | * Retrieves a value from workspace configuration. 174 | */ 175 | get: (key: IWorkspaceConfigKeys): T => { 176 | logger.debug(`Getting workspace config for key: flexpilot.${key}`); 177 | return vscode.workspace.getConfiguration().get(key) as T; 178 | }, 179 | 180 | /** 181 | * Sets a value in workspace configuration. 182 | */ 183 | set: async (key: IWorkspaceConfigKeys, value: T): Promise => { 184 | logger.debug(`Setting workspace config for key: flexpilot.${key}`); 185 | return vscode.workspace 186 | .getConfiguration() 187 | .update(key, value, vscode.ConfigurationTarget.Global); 188 | }, 189 | }; 190 | } 191 | 192 | // Export the storage manager instance 193 | export const storage = new StorageManager(); 194 | -------------------------------------------------------------------------------- /src/tokenizers.ts: -------------------------------------------------------------------------------- 1 | import { Tokenizer } from "@flexpilot-ai/tokenizers"; 2 | import assert from "assert"; 3 | import axios from "axios"; 4 | import { createHash } from "crypto"; 5 | import * as vscode from "vscode"; 6 | import { logger } from "./logger"; 7 | import { storage } from "./storage"; 8 | import { getCompletionModelMetadata } from "./utilities"; 9 | 10 | export class Tokenizers { 11 | /** 12 | * Get the tokenizer metadata for the given model. 13 | * @param {string} model - The name of the model. 14 | */ 15 | private static async metadata(model: string) { 16 | // Get the configuration for the model 17 | const metadata = getCompletionModelMetadata(model); 18 | 19 | // Check if the model configuration exists 20 | if (!metadata) { 21 | throw new Error("No tokenizer URL found for model"); 22 | } 23 | 24 | // Prepare the tokenizer file paths 25 | const globalStorageUri = storage.getContext().globalStorageUri; 26 | const fileId = createHash("sha512") 27 | .update(metadata.tokenizerUrl) 28 | .digest("hex"); 29 | const tokenizerFolder = vscode.Uri.joinPath(globalStorageUri, "tokenizers"); 30 | const tokenizerFileUri = vscode.Uri.joinPath(tokenizerFolder, fileId); 31 | 32 | // Check if the tokenizer folder exists in storage 33 | try { 34 | await vscode.workspace.fs.stat(tokenizerFolder); 35 | } catch (error) { 36 | logger.warn(`Folder not found at: ${tokenizerFolder}`); 37 | logger.error(error as Error); 38 | vscode.workspace.fs.createDirectory(tokenizerFolder); 39 | } 40 | 41 | // Return the metadata and the path to the tokenizer file 42 | return { metadata, tokenizerFileUri }; 43 | } 44 | 45 | /** 46 | * Get the tokenizer for the given model. 47 | * @param {string} model - The name of the model. 48 | */ 49 | public static async get(model: string): Promise { 50 | const { tokenizerFileUri } = await this.metadata(model); 51 | logger.debug(`Loading tokenizer from: ${tokenizerFileUri}`); 52 | return new Tokenizer( 53 | Array.from(await vscode.workspace.fs.readFile(tokenizerFileUri)), 54 | ); 55 | } 56 | 57 | /** 58 | * Download the tokenizer for the given model. 59 | * @param {string} model - The name of the model. 60 | * @returns {Promise} The tokenizer object. 61 | */ 62 | public static async download(model: string): Promise { 63 | const { metadata, tokenizerFileUri } = await this.metadata(model); 64 | const response = await vscode.window.withProgress( 65 | { 66 | location: vscode.ProgressLocation.Notification, 67 | title: "Flexpilot", 68 | cancellable: false, 69 | }, 70 | async (progress) => { 71 | progress.report({ 72 | message: "Downloading tokenizer.json", 73 | }); 74 | return await axios.get(metadata.tokenizerUrl, { 75 | responseType: "arraybuffer", 76 | }); 77 | }, 78 | ); 79 | const byteArray = Array.from(new Uint8Array(response.data)); 80 | const tokenizer = new Tokenizer(byteArray); 81 | assert(tokenizer.encode("test string", false).length > 0); 82 | await vscode.workspace.fs.writeFile( 83 | tokenizerFileUri, 84 | new Uint8Array(response.data), 85 | ); 86 | return tokenizer; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | import { basename } from "path"; 2 | import * as vscode from "vscode"; 3 | import { ALLOWED_COMPLETION_MODELS } from "./constants"; 4 | import { ILanguageConfig } from "./interfaces"; 5 | import { logger } from "./logger"; 6 | 7 | /** 8 | * Set the context value for a given key in the extension context 9 | * @param {string} key - The context key 10 | * @param {any} value - The context value 11 | * @returns {Promise} The promise 12 | */ 13 | export const setContext = async (key: string, value: boolean) => { 14 | await vscode.commands.executeCommand("setContext", `flexpilot:${key}`, value); 15 | logger.debug(`Set context: ${key} = ${value}`); 16 | }; 17 | 18 | /** 19 | * Get the completion model metadata for a given model 20 | * @param {string} model - The model name 21 | * @returns {Promise<(typeof ALLOWED_COMPLETION_MODELS)[number] | undefined>} The completion model metadata 22 | */ 23 | export const getCompletionModelMetadata = ( 24 | model: string, 25 | ): (typeof ALLOWED_COMPLETION_MODELS)[number] | undefined => { 26 | logger.debug("Searching for completion models"); 27 | return ALLOWED_COMPLETION_MODELS.find((metadata) => 28 | new RegExp(metadata.regex).test(model), 29 | ); 30 | }; 31 | 32 | /** 33 | * Check if a file exists and return a boolean 34 | * @param {string} fileUri - The URI of the file 35 | */ 36 | export const isFileExists = async (fileUri: vscode.Uri): Promise => { 37 | try { 38 | return !!(await vscode.workspace.fs.stat(fileUri)); 39 | } catch (error) { 40 | logger.warn(String(error)); 41 | logger.warn(`checkFileExists: ${fileUri} File not found`); 42 | } 43 | return false; 44 | }; 45 | 46 | /** 47 | * Get the terminal type from the terminal object 48 | * @param {vscode.Terminal} terminal - The terminal object 49 | * @returns {string} The type of terminal 50 | */ 51 | export const getTerminalType = (terminal: vscode.Terminal): string => { 52 | logger.debug("Getting terminal type"); 53 | if ( 54 | terminal && 55 | "shellPath" in terminal.creationOptions && 56 | terminal.creationOptions.shellPath 57 | ) { 58 | const shellName = basename(terminal.creationOptions.shellPath); 59 | switch (true) { 60 | case shellName === "bash.exe": 61 | logger.debug("Terminal type: Git Bash"); 62 | return "Git Bash"; 63 | case shellName.startsWith("pwsh"): 64 | case shellName.startsWith("powershell"): 65 | logger.debug("Terminal type: powershell"); 66 | return "powershell"; 67 | case Boolean(shellName.trim()): 68 | logger.debug(`Terminal type: ${shellName.split(".")[0]}`); 69 | return shellName.split(".")[0]; 70 | } 71 | } 72 | const defaultType = process.platform === "win32" ? "powershell" : "bash"; 73 | logger.debug(`Returning terminal type: ${defaultType}`); 74 | return defaultType; 75 | }; 76 | 77 | /** 78 | * Get the end of line sequence for a document 79 | * @param {vscode.TextDocument} document - The document 80 | * @returns {string} The end of line sequence 81 | */ 82 | export const getEol = (document: vscode.TextDocument): string => { 83 | logger.debug("Getting end of line sequence for document"); 84 | return document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n"; 85 | }; 86 | 87 | /** 88 | * Retrieves the language configuration for a given language ID. 89 | * @param {string} languageId - The ID of the language. 90 | * @returns {ILanguageConfig} The configuration for the specified language. 91 | */ 92 | export const getLanguageConfig = (languageId: string): ILanguageConfig => { 93 | if (LANGUAGES[languageId]) { 94 | return LANGUAGES[languageId]; 95 | } else { 96 | return { markdown: languageId, comment: { start: "//", end: "" } }; 97 | } 98 | }; 99 | 100 | /** 101 | * A mapping of language IDs to their respective configurations. 102 | */ 103 | const LANGUAGES: { [key: string]: ILanguageConfig } = { 104 | abap: { 105 | markdown: "abap", 106 | comment: { start: "* ", end: " */" }, 107 | }, 108 | bibtex: { 109 | markdown: "bibtex", 110 | comment: { start: "% ", end: "" }, 111 | }, 112 | d: { 113 | markdown: "d", 114 | comment: { start: "/* ", end: " */" }, 115 | }, 116 | pascal: { 117 | markdown: "pascal", 118 | comment: { start: "{ ", end: " }" }, 119 | }, 120 | erlang: { 121 | markdown: "erlang", 122 | comment: { start: "%% ", end: " %%" }, 123 | }, 124 | haml: { 125 | markdown: "haml", 126 | comment: { start: "-# ", end: " -#" }, 127 | }, 128 | haskell: { 129 | markdown: "haskell", 130 | comment: { start: "{- ", end: " -}" }, 131 | }, 132 | ocaml: { 133 | markdown: "ocaml", 134 | comment: { start: "(* ", end: " *)" }, 135 | }, 136 | perl6: { 137 | markdown: "perl6", 138 | comment: { start: "/* ", end: " */" }, 139 | }, 140 | sass: { 141 | markdown: "scss", 142 | comment: { start: "/* ", end: " */" }, 143 | }, 144 | slim: { 145 | markdown: "slim", 146 | comment: { start: "/ ", end: "" }, 147 | }, 148 | stylus: { 149 | markdown: "stylus", 150 | comment: { start: "// ", end: "" }, 151 | }, 152 | svelte: { 153 | markdown: "svelte", 154 | comment: { start: "/* ", end: " */" }, 155 | }, 156 | vue: { 157 | markdown: "vue", 158 | comment: { start: "/* ", end: " */" }, 159 | }, 160 | "vue-html": { 161 | markdown: "html", 162 | comment: { start: "" }, 163 | }, 164 | razor: { 165 | markdown: "razor", 166 | comment: { start: "" }, 167 | }, 168 | shaderlab: { 169 | markdown: "shader", 170 | comment: { start: "/* ", end: " */" }, 171 | }, 172 | dockerfile: { 173 | markdown: "dockerfile", 174 | comment: { start: "# ", end: "" }, 175 | }, 176 | go: { 177 | markdown: "go", 178 | comment: { start: "/* ", end: " */" }, 179 | }, 180 | python: { 181 | markdown: "py", 182 | comment: { start: '""" ', end: ' """' }, 183 | }, 184 | css: { 185 | markdown: "css", 186 | comment: { start: "/* ", end: " */" }, 187 | }, 188 | clojure: { 189 | markdown: "clj", 190 | comment: { start: ";; ", end: "" }, 191 | }, 192 | less: { 193 | markdown: "less", 194 | comment: { start: "/* ", end: " */" }, 195 | }, 196 | dart: { 197 | markdown: "dart", 198 | comment: { start: "/* ", end: " */" }, 199 | }, 200 | tex: { 201 | markdown: "tex", 202 | comment: { start: "% ", end: "" }, 203 | }, 204 | latex: { 205 | markdown: "latex", 206 | comment: { start: "% ", end: "" }, 207 | }, 208 | scss: { 209 | markdown: "scss", 210 | comment: { start: "/* ", end: " */" }, 211 | }, 212 | perl: { 213 | markdown: "pl", 214 | comment: { start: "# ", end: "" }, 215 | }, 216 | raku: { 217 | markdown: "raku", 218 | comment: { start: "# ", end: "" }, 219 | }, 220 | rust: { 221 | markdown: "rs", 222 | comment: { start: "/* ", end: " */" }, 223 | }, 224 | jade: { 225 | markdown: "pug", 226 | comment: { start: "//- ", end: "" }, 227 | }, 228 | fsharp: { 229 | markdown: "fs", 230 | comment: { start: "(* ", end: " *)" }, 231 | }, 232 | r: { 233 | markdown: "r", 234 | comment: { start: "# ", end: "" }, 235 | }, 236 | java: { 237 | markdown: "java", 238 | comment: { start: "/* ", end: " */" }, 239 | }, 240 | diff: { 241 | markdown: "diff", 242 | comment: { start: "# ", end: " " }, 243 | }, 244 | html: { 245 | markdown: "html", 246 | comment: { start: "" }, 247 | }, 248 | php: { 249 | markdown: "php", 250 | comment: { start: "/* ", end: " */" }, 251 | }, 252 | lua: { 253 | markdown: "lua", 254 | comment: { start: "--[[ ", end: " ]]" }, 255 | }, 256 | xml: { 257 | markdown: "xml", 258 | comment: { start: "" }, 259 | }, 260 | xsl: { 261 | markdown: "xsl", 262 | comment: { start: "" }, 263 | }, 264 | vb: { 265 | markdown: "vb", 266 | comment: { start: "' ", end: "" }, 267 | }, 268 | powershell: { 269 | markdown: "ps1", 270 | comment: { start: "<# ", end: " #>" }, 271 | }, 272 | typescript: { 273 | markdown: "ts", 274 | comment: { start: "/* ", end: " */" }, 275 | }, 276 | typescriptreact: { 277 | markdown: "tsx", 278 | comment: { start: "/* ", end: " */" }, 279 | }, 280 | ini: { 281 | markdown: "ini", 282 | comment: { start: "; ", end: " " }, 283 | }, 284 | properties: { 285 | markdown: "conf", 286 | comment: { start: "# ", end: " " }, 287 | }, 288 | json: { 289 | markdown: "json", 290 | comment: { start: "/* ", end: " */" }, 291 | }, 292 | jsonc: { 293 | markdown: "jsonc", 294 | comment: { start: "/* ", end: " */" }, 295 | }, 296 | jsonl: { 297 | markdown: "jsonl", 298 | comment: { start: "/* ", end: " */" }, 299 | }, 300 | snippets: { 301 | markdown: "code-snippets", 302 | comment: { start: "/* ", end: " */" }, 303 | }, 304 | "git-commit": { 305 | markdown: "git-commit", 306 | comment: { start: "# ", end: " " }, 307 | }, 308 | "git-rebase": { 309 | markdown: "git-rebase", 310 | comment: { start: "# ", end: " " }, 311 | }, 312 | ignore: { 313 | markdown: "gitignore_global", 314 | comment: { start: "# ", end: "" }, 315 | }, 316 | handlebars: { 317 | markdown: "handlebars", 318 | comment: { start: "{{!-- ", end: " --}}" }, 319 | }, 320 | c: { 321 | markdown: "c", 322 | comment: { start: "/* ", end: " */" }, 323 | }, 324 | cpp: { 325 | markdown: "cpp", 326 | comment: { start: "/* ", end: " */" }, 327 | }, 328 | "cuda-cpp": { 329 | markdown: "cpp", 330 | comment: { start: "/* ", end: " */" }, 331 | }, 332 | swift: { 333 | markdown: "swift", 334 | comment: { start: "/* ", end: " */" }, 335 | }, 336 | makefile: { 337 | markdown: "mak", 338 | comment: { start: "# ", end: "" }, 339 | }, 340 | shellscript: { 341 | markdown: "sh", 342 | comment: { start: "# ", end: "" }, 343 | }, 344 | markdown: { 345 | markdown: "md", 346 | comment: { start: "" }, 347 | }, 348 | dockercompose: { 349 | markdown: "dockercompose", 350 | comment: { start: "# ", end: "" }, 351 | }, 352 | yaml: { 353 | markdown: "yaml", 354 | comment: { start: "# ", end: "" }, 355 | }, 356 | csharp: { 357 | markdown: "cs", 358 | comment: { start: "/* ", end: " */" }, 359 | }, 360 | julia: { 361 | markdown: "jl", 362 | comment: { start: "#= ", end: " =#" }, 363 | }, 364 | bat: { 365 | markdown: "bat", 366 | comment: { start: "@REM ", end: "" }, 367 | }, 368 | groovy: { 369 | markdown: "groovy", 370 | comment: { start: "/* ", end: " */" }, 371 | }, 372 | coffeescript: { 373 | markdown: "coffee", 374 | comment: { start: "### ", end: " ###" }, 375 | }, 376 | javascriptreact: { 377 | markdown: "jsx", 378 | comment: { start: "/* ", end: " */" }, 379 | }, 380 | javascript: { 381 | markdown: "js", 382 | comment: { start: "/* ", end: " */" }, 383 | }, 384 | "jsx-tags": { 385 | markdown: "jsx-tags", 386 | comment: { start: "{/* ", end: " */}" }, 387 | }, 388 | hlsl: { 389 | markdown: "hlsl", 390 | comment: { start: "/* ", end: " */" }, 391 | }, 392 | restructuredtext: { 393 | markdown: "rst", 394 | comment: { start: ".. ", end: "" }, 395 | }, 396 | "objective-c": { 397 | markdown: "m", 398 | comment: { start: "/* ", end: " */" }, 399 | }, 400 | "objective-cpp": { 401 | markdown: "cpp", 402 | comment: { start: "/* ", end: " */" }, 403 | }, 404 | ruby: { 405 | markdown: "rb", 406 | comment: { start: "=begin ", end: " =end" }, 407 | }, 408 | sql: { 409 | markdown: "sql", 410 | comment: { start: "/* ", end: " */" }, 411 | }, 412 | }; 413 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["launch_folder"], 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "module": "Node16", 6 | "target": "ES2022", 7 | "lib": ["ES2022"], 8 | "sourceMap": true, 9 | "skipLibCheck": true, 10 | "rootDir": "src", 11 | "jsx": "react-jsx", 12 | "strict": true /* enable all strict type-checking options */ 13 | /* Additional Checks */ 14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import path from "path"; 3 | 4 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 5 | 6 | /** @type WebpackConfig */ 7 | const extensionConfig = { 8 | target: "node", // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 9 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 10 | 11 | entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'out' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(path.dirname(new URL(import.meta.url).pathname), "out"), 15 | filename: "extension.js", 16 | clean: true, 17 | libraryTarget: "commonjs2", 18 | }, 19 | externals: { 20 | 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/ 21 | // modules added here also need to be added in the .vscodeignore file 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: [".ts", ".js", ".json", ".tsx", ".node"], 26 | extensionAlias: { 27 | ".js": [".ts", ".js"], 28 | }, 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.node$/, 34 | use: [ 35 | { 36 | loader: "node-loader", 37 | options: { name: "[name].[ext]" }, 38 | }, 39 | ], 40 | }, 41 | { 42 | test: /\.(ts|tsx)$/, 43 | exclude: /node_modules/, 44 | use: [ 45 | { 46 | loader: "ts-loader", 47 | }, 48 | ], 49 | }, 50 | { 51 | test: /\.json$/, 52 | type: "json", 53 | }, 54 | ], 55 | }, 56 | devtool: "nosources-source-map", 57 | infrastructureLogging: { 58 | level: "log", // enables logging required for problem matchers 59 | }, 60 | }; 61 | export default [extensionConfig]; 62 | --------------------------------------------------------------------------------