├── .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 | 
15 | [](LICENSE)
16 | 
17 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |

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