├── .eslintrc.json ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── icon.jpg ├── package-lock.json ├── package.json ├── src ├── anthropic.ts ├── apiProvider.ts ├── atom-one-dark.min.css ├── chatide.css ├── chatide.html ├── chatide.js ├── custom.ts ├── extension.ts ├── highlight.min.js ├── openai.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts └── utils.ts ├── tsconfig.json ├── vsc-extension-quickstart.md └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off", 18 | "indent": ["error", 4] 19 | }, 20 | "ignorePatterns": [ 21 | "out", 22 | "dist", 23 | "**/*.d.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to VS Code Extensions Marketplace 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.md' 9 | - '.github/workflows/release.yml' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 16 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Install vsce 27 | run: npm install --global @vscode/vsce 28 | 29 | - name: Package VSIX 30 | run: vsce package 31 | 32 | - name: Publish to Marketplace 33 | run: vsce publish -p ${{ secrets.MS_AZURE_PAT }} 34 | env: 35 | PUBLISHER_ID: chatIDE 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .DS_Store 4 | *.vsix 5 | gpt-conversations/ 6 | ignore/ 7 | .env 8 | Dockerfile -------------------------------------------------------------------------------- /.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 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/**/*.js", 30 | "${workspaceFolder}/dist/**/*.js" 31 | ], 32 | "preLaunchTask": "tasks: watch-tests" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /.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 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": [ 34 | "npm: watch", 35 | "npm: watch-tests" 36 | ], 37 | "problemMatcher": [] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | out/test 4 | **/*.map 5 | **/*.ts 6 | **/*.js.map 7 | **/*.tsbuildinfo -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "ChatIDE" extension will be documented in this file. 4 | 5 | ## [0.2.8] 6 | ### Bug fix 7 | - Changing OpenAI API key would sometimes not work 8 | 9 | ## [0.2.7] 10 | 11 | ### Added 12 | - New setting that enables sending messages using Enter key press (enable in settings) 13 | - Support for custom server URL (must be OpenAI 'compatible') 14 | 15 | ## [0.2.6] 16 | 17 | ### Added 18 | - Support for Claude 100K context length ("claude-v1.3-100k") 19 | - Support for GPT4 32K context length ("gpt-4-32k") 20 | 21 | ## [0.2.0] 22 | 23 | ### Added 24 | - Support for Anthropic's Claude 25 | 26 | ## [0.1.0] 27 | 28 | ### Added 29 | - Use highlight.js to highlight code blocks 30 | - "copy code" button to easily copy code produced by the assistant 31 | - Highlighting code automatically includes it in a special "context" prefix message to the assistant (this happens only once per code selection) 32 | - Pressing tab in the textrea now inserts a tab into the text instead of changing focus target 33 | 34 | ## [0.0.9] 35 | 36 | ### Added 37 | - Render the user message as markdown 38 | 39 | ## [0.0.8] 40 | 41 | ### Changed 42 | - Change default model to gpt-3.5-turbo because most people don't have access to 4 yet 43 | 44 | ### Added 45 | - Show OpenAI API error in the chat with some troubleshooting options 46 | 47 | ## [0.0.7] 48 | 49 | ### Added 50 | - Load and continue a conversation from JSON file 51 | - Styling updates 52 | 53 | ## [0.0.6] 54 | 55 | ### Fixed 56 | - Fix a bug where the api key won't be registered until the user restarted VS Code 57 | 58 | ## [0.0.5] 59 | 60 | ### Added 61 | - Add code to publish the extension to Microsoft's extension marketplace 62 | 63 | ## [0.0.4] 64 | 65 | ### Added 66 | - Store API key in VS Code secretStorage 67 | - System prompt asks GPT to avoid repeating information 68 | 69 | ## [0.0.3] 70 | 71 | ### Added 72 | - Ability to configure system prompt through extension settings 73 | - Ability to reset chat 74 | - Ability to export current chat as JSON 75 | 76 | ## [Unreleased] 77 | 78 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yagil Burowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | image 5 | 6 |

7 |

8 | 9 | Installs 10 | 11 | 12 | 13 | Github repo stars 14 | 15 |

16 |

17 | ChatIDE - AI assistant in your IDE
18 | Converse with OpenAI's ChatGPT or Anthropic's Claude in VSCode 19 |

20 | 21 |

22 | 23 | image 24 | 25 |

26 | 27 |

28 | 29 | License 30 | 31 |

32 | 33 | [![Video demo](https://chatide.dev/assets/example.png)](https://user-images.githubusercontent.com/3611042/232339222-c0532f49-6772-46c5-b29e-be00f77c1c1c.mov) 34 | 35 | ## Build and run from source 36 | 37 | 1. Clone this repository and install the npm dependencies 38 | 2. Open the project in VS Code 39 | 3. Press `F5` to launch the extension in debug mode 40 | 41 | ## Installation 42 | 43 | Grab the latest ChatIDE version from the Extensions Marketplace:
44 | https://marketplace.visualstudio.com/items?itemName=ChatIDE.chatide 45 | 46 | ## Bring Your Own API keys 47 | 48 | To use ChatGPT / Claude in ChatIDE, you need to procure API Keys from OpenAI / Anthropic. 49 | - **OpenAI**: https://openai.com/product#made-for-developers 50 | - **Anthropic**: https://console.anthropic.com/docs/api 51 | 52 | ## Usage 53 | 54 | 1. Bring up ChatIDE with `Cmd + Shift + i` (or `Ctrl + Shift + i` on non-Apple platforms). 55 | 2. Choose your AI model. Currently supported: 56 | - `'gpt-4'`, `'gpt-4-0613'`,`'gpt-3.5-turbo'`,`'gpt-3.5-turbo-16k'` (OpenAI) 57 | - `'claude-v1.3'` (Anthropic) 58 | 4. On first usage, you'll be prompted to enter your API key for your chosen AI providers (will be stored in VSCode `secretStorage`). 59 | 5. Enjoy! 60 | 61 | ## Configuration 62 | 63 | - Use the `Cmd + Shift + P` keychord and type `>Open ChatIDE Settings` 64 | - Choose your preferred `model`, `max_tokens`, and `temperature`. 65 | - Adjust the system prompt to your liking 66 | - Note: settings will auto save 67 | - Run ChatIDE with `Cmd + Shift + i`. You'll be asked for your OpenAI / Anthropic API key on first time you use the model. 68 | - Note: your API keys will be stored in [VS Code's `secretStorage`](https://code.visualstudio.com/api/references/vscode-api#SecretStorage) 69 |

70 | image 71 |

72 | 73 | ### Updating your API key 74 | 75 | 1. Run `cmd + shift + P` (or `ctrl + shift + P`) 76 | 2. Start typing `>ChatIDE` 77 | 78 | #### OpenAI 79 | 3. Select `>Update your OpenAI API Key for ChatIDE`. 80 | 81 | #### Anthropic 82 | 3. Select `>Update your Anthropic API Key for ChatIDE`. 83 | 84 | ## Known issues 85 | 86 | 1. There's currently no way to stop the model from generating. You need to wait until it's done. 87 | 2. Closing the ChatIDE pane while the model is generating might lead to a non-recoverable error. You'll need to restart VS Code to use ChatIDE again. 88 | 89 | ## Warning 90 | 91 | ⚠️ This is an early prototype, use at your own peril. 92 | 93 | 🧐 Remember to keep an eye on your OpenAI / Anthropic billing. 94 | 95 | ## Credits 96 | 97 | ChatIDE continues to be built using ChatIDE. 98 | -------------------------------------------------------------------------------- /assets/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yagil/ChatIDE/a0a58fe96fbd39b3d4d71def16c37e4226f36224/assets/icon.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatide", 3 | "publisher": "ChatIDE", 4 | "displayName": "ChatIDE - Coding Assistant (GPT/ChatGPT, Claude)", 5 | "description": "ChatIDE is an open-source coding and debugging assistant that supports GPT/ChatGPT (OpenAI), and Claude (Anthropic). Supported models: [gpt4, gpt-3.5-turbo, claude-v1.3]. Import/export your conversation history. Bring up the assistant in a side pane by pressing cmd+shift+i.", 6 | "version": "0.3.5", 7 | "license": "MIT", 8 | "icon": "assets/icon.jpg", 9 | "engines": { 10 | "vscode": "^1.77.0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/yagil/ChatIDE" 15 | }, 16 | "categories": [ 17 | "Other" 18 | ], 19 | "keywords": [ 20 | "anthropic", 21 | "claude", 22 | "chatgpt", 23 | "gpt", 24 | "gpt4", 25 | "gpt3", 26 | "openai", 27 | "ai", 28 | "agi", 29 | "artificial-intelligence", 30 | "natural-language-processing", 31 | "nlp", 32 | "language-model", 33 | "coding-assistant", 34 | "programming-help", 35 | "debugging", 36 | "questions", 37 | "code-generation" 38 | ], 39 | "activationEvents": [], 40 | "main": "./dist/extension.js", 41 | "contributes": { 42 | "commands": [ 43 | { 44 | "command": "chatide.start", 45 | "title": "Start ChatIDE" 46 | }, 47 | { 48 | "command": "chatide.openSettings", 49 | "title": "Open ChatIDE Settings" 50 | }, 51 | { 52 | "command": "chatide.updateOpenAiApiKey", 53 | "title": "Update your OpenAI API Key for ChatIDE" 54 | }, 55 | { 56 | "command": "chatide.updateAnthropicApiKey", 57 | "title": "Update your Anthropic API Key for ChatIDE" 58 | } 59 | ], 60 | "keybindings": [ 61 | { 62 | "key": "cmd+shift+i", 63 | "command": "chatide.start", 64 | "when": "editorTextFocus" 65 | } 66 | ], 67 | "configuration": { 68 | "title": "ChatIDE", 69 | "properties": { 70 | "chatide.model": { 71 | "order": 0, 72 | "type": "string", 73 | "default": "gpt-4o", 74 | "enum": [ 75 | "gpt-4o", 76 | "claude-3-5-sonnet-20240620", 77 | "gpt-4o-2024-08-06", 78 | "claude-3-opus-20240229", 79 | "chatgpt-4o-latest", 80 | "gpt-4-turbo", 81 | "gpt-4", 82 | "custom" 83 | ], 84 | "description": "Select the AI model. You must have your own API key to use a model.", 85 | "scope": "window" 86 | }, 87 | "chatide.systemPrompt": { 88 | "order": 1, 89 | "type": "string", 90 | "default": "You are a helpful programming assistant running inside VS Code. The assistant always generates accurate and succinct messages. The assistant doesn't repeat previous information, and doesn't launch into long explanations unless asked. Always provide code inside triple backticks.", 91 | "markdownDescription": "The 'system' prompt provided to the model. Keep in mind that some models pay stronger attention to the system prompt than others. For models that don't have a dedciated affordance for system prompts, this will be prepended to the first user prompt.", 92 | "scope": "window", 93 | "editorType": "string", 94 | "editorMultiline": true 95 | }, 96 | "chatide.maxLength": { 97 | "order": 2, 98 | "type": "integer", 99 | "default": 1000, 100 | "minimum": 1, 101 | "description": "Set the maximum length for the generated text (aka max_tokens).", 102 | "scope": "window" 103 | }, 104 | "chatide.temperature": { 105 | "order": 3, 106 | "type": "number", 107 | "default": 0, 108 | "maximum": 1, 109 | "minimum": 0, 110 | "description": "Set the temperature for controlling the randomness of the generated text.", 111 | "scope": "window" 112 | }, 113 | "chatide.highlightedCodeAwareness": { 114 | "order": 4, 115 | "type": "boolean", 116 | "default": false, 117 | "description": "[Experimental] Automatically include highlighted code in the prompt.", 118 | "scope": "window" 119 | }, 120 | "chatide.pressEnterToSend": { 121 | "order": 5, 122 | "type": "boolean", 123 | "default": false, 124 | "description": "When enabled, pressing enter will send the message in the input box. Shift + Enter will insert a new line. When this option is not selected, you must click the 'Send' button.", 125 | "scope": "window" 126 | }, 127 | "chatide.customServerUrl": { 128 | "order": 6, 129 | "type": "string", 130 | "description": "Custom OpenAI-compatible server URL (Example http://localhost:1234/v1). Note: '/chat/completions' will be appended to the URL.", 131 | "scope": "window", 132 | "editorType": "string" 133 | }, 134 | "chatide.autoSaveDirectory": { 135 | "order": 0, 136 | "type": "string", 137 | "default": "~/.cache/chatide/", 138 | "description": "The directory where the conversation will be automatically saved. If empty, auto-save will be disabled.", 139 | "scope": "window" 140 | }, 141 | "chatide.autoSaveEnabled": { 142 | "type": "boolean", 143 | "default": true, 144 | "description": "When enabled, the conversation will be automatically saved to the directory specified in 'chatide.autoSaveDirectory'.", 145 | "scope": "window" 146 | } 147 | } 148 | } 149 | }, 150 | "scripts": { 151 | "vscode:prepublish": "npm run package", 152 | "compile": "webpack", 153 | "watch": "webpack --watch", 154 | "package": "webpack --mode production --devtool hidden-source-map", 155 | "compile-tests": "tsc -p . --outDir out", 156 | "watch-tests": "tsc -p . -w --outDir out", 157 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 158 | "lint": "eslint src --ext ts", 159 | "test": "node ./out/test/runTest.js" 160 | }, 161 | "devDependencies": { 162 | "@types/glob": "^8.1.0", 163 | "@types/marked": "^4.0.8", 164 | "@types/mocha": "^10.0.1", 165 | "@types/node": "16.x", 166 | "@types/vscode": "^1.77.0", 167 | "@typescript-eslint/eslint-plugin": "^5.56.0", 168 | "@typescript-eslint/parser": "^5.56.0", 169 | "@vscode/test-electron": "^2.3.0", 170 | "eslint": "^8.36.0", 171 | "glob": "^8.1.0", 172 | "mocha": "^10.2.0", 173 | "ts-loader": "^9.4.2", 174 | "typescript": "^4.9.5", 175 | "webpack": "^5.76.3", 176 | "webpack-cli": "^5.0.1" 177 | }, 178 | "dependencies": { 179 | "@anthropic-ai/sdk": "^0.17.1", 180 | "dotenv": "^16.0.3", 181 | "marked": "^4.3.0", 182 | "openai": "^3.2.1" 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/anthropic.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from "vscode"; 3 | import { APIProvider } from "./apiProvider"; 4 | 5 | import Anthropic from '@anthropic-ai/sdk'; 6 | import { MessageParam } from "@anthropic-ai/sdk/resources"; 7 | 8 | export interface AnthropicParams { 9 | messages: Array; 10 | max_tokens: number; 11 | model: string; 12 | stream: boolean; 13 | temperature?: number; 14 | } 15 | 16 | export class AnthropicProvider extends APIProvider { 17 | private client: Anthropic | undefined; 18 | private context: vscode.ExtensionContext; 19 | 20 | constructor(context: vscode.ExtensionContext) { 21 | super(); 22 | this.context = context; 23 | } 24 | 25 | async init() { 26 | let apiKey = await this.context.secrets.get("chatide.anthropicApiKey"); 27 | if (!apiKey) { 28 | apiKey = await vscode.window.showInputBox({ 29 | prompt: "Enter your Anthropic API key:", 30 | ignoreFocusOut: true, 31 | }); 32 | if (apiKey) { 33 | await this.context.secrets.store("chatide.anthropicApiKey", apiKey); 34 | } else { 35 | throw new Error("No API key provided. Please add your API key and restart the extension."); 36 | } 37 | } 38 | 39 | this.client = new Anthropic({ 40 | apiKey 41 | }); 42 | } 43 | 44 | async completeStream(params: AnthropicParams, callbacks: any) { 45 | if (!this.client) { 46 | throw new Error("Anthropic API client is not initialized."); 47 | } 48 | 49 | let anthropicMessage = ""; 50 | // @ts-ignore 51 | const systemMessage = params.messages.find((message) => message.role === "system"); 52 | // @ts-ignore 53 | const messagesWithoutSystem = params.messages.filter((message) => message.role !== "system"); 54 | try { 55 | const stream = await this.client.messages.create({ 56 | max_tokens: params.max_tokens, 57 | system: systemMessage?.content as string|undefined, 58 | messages: messagesWithoutSystem, 59 | model: params.model, 60 | stream: params.stream, 61 | }); 62 | if (callbacks.onComplete) { 63 | // @ts-ignore 64 | for await (const messageStreamEvent of stream) { 65 | const { type, delta } = messageStreamEvent; 66 | if (type === "content_block_delta") { 67 | anthropicMessage += delta.text; 68 | callbacks.onUpdate(anthropicMessage); 69 | } else if (type === "message_stop") { 70 | console.log("MessageStreamEvent:", messageStreamEvent); 71 | callbacks.onComplete(anthropicMessage); 72 | } 73 | } 74 | } 75 | } catch (error: any) { 76 | console.error("Error fetching stream:", error); 77 | throw error; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/apiProvider.ts: -------------------------------------------------------------------------------- 1 | export abstract class APIProvider { 2 | abstract init(): Promise; 3 | abstract completeStream( 4 | params: any, 5 | callbacks: { 6 | onOpen?: (response: any) => void; 7 | onUpdate?: (completion: any) => void; 8 | onComplete?: (message: any) => void; 9 | } 10 | ): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/atom-one-dark.min.css: -------------------------------------------------------------------------------- 1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline} -------------------------------------------------------------------------------- /src/chatide.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 7 | background-color: #161616; 8 | } 9 | 10 | pre, 11 | code { 12 | white-space: pre-wrap; 13 | word-wrap: break-word; 14 | overflow-wrap: break-word; 15 | } 16 | 17 | #chat-container { 18 | display: flex; 19 | flex-direction: column; 20 | height: 100%; 21 | } 22 | 23 | #chat-header { 24 | display: flex; 25 | align-items: center; 26 | justify-content: space-between; 27 | background: black; 28 | padding: 16px; 29 | } 30 | 31 | h1#chat-title { 32 | margin: 0; 33 | font-weight: 500; 34 | color: white; 35 | font-size: 1.3em; 36 | } 37 | 38 | #config-details { 39 | color:#ccc; 40 | font-size: x-small; 41 | } 42 | 43 | #model-prefix { 44 | color: #888; 45 | } 46 | 47 | #messages { 48 | flex-grow: 1; 49 | overflow-y: auto; 50 | padding: 16px; 51 | word-break: break-word; /* break words if necessary */ 52 | } 53 | 54 | .user-message, 55 | .assistant-message, 56 | .system-message, 57 | .extension-message { 58 | margin-bottom: 8px; 59 | padding: 8px; 60 | word-wrap: break-word; /* break words if necessary */ 61 | border-radius: 5px; 62 | max-width: 100%; /* limit the width to 100% of the container */ 63 | overflow-wrap: break-word; /* break long words if necessary */ 64 | } 65 | 66 | #chat-control { 67 | display: flex; 68 | flex-direction: row; 69 | align-items: center; 70 | } 71 | 72 | .control-btn { 73 | font-size: smaller; 74 | padding: 5px 10px; 75 | border-radius: 5px; 76 | background-color: #1d1d1d; 77 | color: rgb(195, 195, 195); 78 | border: none; 79 | cursor: pointer; 80 | margin-right: 10px; 81 | white-space: nowrap; 82 | } 83 | 84 | .control-btn:hover { 85 | background-color: #292929; 86 | color:white; 87 | } 88 | 89 | .user-message { 90 | background-color: rgb(87, 87, 87); 91 | color: white; 92 | } 93 | 94 | .assistant-message { 95 | background-color: #333; 96 | color: white; 97 | } 98 | 99 | .system-message { 100 | background-color: #332d45; 101 | color: rgb(178, 120, 241); 102 | margin-bottom: 20px !important; 103 | } 104 | 105 | .extension-message { 106 | background-color: #332d45; 107 | color: rgb(241, 120, 120); 108 | margin-bottom: 20px !important; 109 | } 110 | 111 | #message-input { 112 | width: 90%; 113 | min-height: 36px; 114 | max-height: 160px; /* limit the maximum height */ 115 | resize: none; 116 | outline: none; 117 | border: 1px solid #ccc; 118 | padding: 8px; 119 | box-sizing: border-box; 120 | overflow-y: auto; /* allow scrolling when the content exceeds the maximum height */ 121 | white-space: pre-wrap; /* wrap the text */ 122 | word-wrap: break-word; /* break words if necessary */ 123 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 124 | font-size: 13px; 125 | } 126 | 127 | #send-button { 128 | width: 10%; 129 | min-height: 36px; 130 | background-color: #007acc; 131 | color: white; 132 | border: none; 133 | cursor: pointer; 134 | font-size: 1em; 135 | } 136 | 137 | #send-button:hover { 138 | background-color: #005999; 139 | } 140 | 141 | .chat-bar { 142 | display: flex; 143 | padding: 8px; 144 | } 145 | 146 | #chat-logo { 147 | width: 20px; 148 | border-radius: 2px; 149 | margin-right: 8px; 150 | } 151 | 152 | #logo-container { 153 | display: flex; 154 | align-items: center; 155 | flex-direction: row; 156 | } 157 | 158 | b { 159 | font-weight: medium; 160 | } 161 | 162 | #status-bar { 163 | font-size: 0.8em; 164 | color: #888; 165 | display: flex; 166 | align-items: center; 167 | justify-content: space-between; 168 | padding: 4px 10px; 169 | background-color: #333; 170 | border-top: 1px solid #444; 171 | height: 20px; 172 | } 173 | 174 | #status-bar a, 175 | #status-bar button { 176 | color: #fff; 177 | text-decoration: underline; 178 | background: none; 179 | border: none; 180 | padding: 0; 181 | margin: 0; 182 | cursor: pointer; 183 | } 184 | 185 | #status-bar button:hover { 186 | text-decoration: none; 187 | } 188 | 189 | .white-text { 190 | color: white; 191 | } 192 | 193 | .code-block-wrapper { 194 | position: relative; 195 | display: block; 196 | margin-bottom: 1em; 197 | background-color: #282c34; 198 | padding: 0.5em; 199 | border-radius: 5px; 200 | } 201 | 202 | .copy-code-button { 203 | position: absolute; 204 | top: 0; 205 | right: 0; 206 | margin: 10px 10px 0 0; 207 | font-size: 12px; 208 | padding: 5px 7px; 209 | background-color: #353840; 210 | border-radius: 5px; 211 | color: white; 212 | border: none; 213 | cursor: pointer; 214 | z-index: 1; 215 | } 216 | 217 | #config-container { 218 | display: flex; 219 | flex-direction: row; 220 | justify-content: space-between; 221 | cursor: pointer; 222 | } 223 | 224 | #settings-button { 225 | background-color:black; 226 | border: none; 227 | margin-left: 5px; 228 | color: white; 229 | font-size:large; 230 | cursor: pointer; 231 | } -------------------------------------------------------------------------------- /src/chatide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ChatIDE 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 |

ChatIDE

15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |

Model: ${modelConfigDetails}

23 | 24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 |
32 | ${codeHighlightStatusCopy} 33 | 34 |
35 | 36 | 37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/chatide.js: -------------------------------------------------------------------------------- 1 | const vscode = acquireVsCodeApi(); 2 | 3 | let gPreferences = {}; 4 | 5 | document.addEventListener("DOMContentLoaded", () => { 6 | const messagesContainer = document.getElementById("messages"); 7 | 8 | document.getElementById("send-button").addEventListener("click", sendMessage); 9 | 10 | // listen for enter and enter + shift: 11 | document.getElementById("message-input").addEventListener("keydown", (e) => { 12 | // if "pressEnterToSend" is in gPreferences, get it: 13 | const pressEnterToSend = gPreferences.pressEnterToSend ?? false; 14 | 15 | if (pressEnterToSend && e.key === "Enter" && !e.shiftKey) { 16 | e.preventDefault(); 17 | sendMessage(); 18 | } 19 | }); 20 | 21 | const messageInputTextArea = document.getElementById('message-input'); 22 | handleTabInTextarea(messageInputTextArea); 23 | 24 | messageInputTextArea.addEventListener('input', function () { 25 | autoResize(this); 26 | }); 27 | 28 | document.getElementById('reset-button').addEventListener('click', () => { 29 | vscode.postMessage({ 30 | command: 'resetChat' 31 | }); 32 | }); 33 | 34 | document.getElementById('auto-save-checkbox').addEventListener('change', (event) => { 35 | vscode.postMessage({ 36 | command: 'toggleAutoSave', 37 | enabled: event.target.checked 38 | }); 39 | }); 40 | 41 | document.getElementById('export-button').addEventListener('click', () => { 42 | vscode.postMessage({ 43 | command: 'exportChat' 44 | }); 45 | }); 46 | 47 | document.getElementById('import-button').addEventListener('click', () => { 48 | vscode.postMessage({ 49 | command: 'importChat' 50 | }); 51 | }); 52 | 53 | document.getElementById('config-container').addEventListener('click', () => { 54 | vscode.postMessage({ 55 | command: 'openSettings' 56 | }); 57 | }); 58 | 59 | document.getElementById('show-code').addEventListener('click', () => { 60 | vscode.postMessage({ command: 'navigateToHighlightedCode' }); 61 | }); 62 | 63 | window.addEventListener("message", (event) => { 64 | const message = event.data; 65 | switch (message.command) { 66 | case "sentUserMessage": 67 | addMessage("user", message.userMessageMarkdown); 68 | break; 69 | case "gptResponse": 70 | addMessage("assistant", message.token, true); 71 | break; 72 | case "resetChatComplete": 73 | messagesContainer.innerHTML = ""; 74 | break; 75 | case "loadChatComplete": 76 | message.messages.forEach((message) => { 77 | addMessage(message.role, message.content); 78 | }); 79 | break; 80 | case "openAiError": 81 | addMessage("extension", message.error); 82 | break; 83 | case "updateHighlightedCodeStatus": 84 | document.getElementById('highlighted-code-status').textContent = message.status; 85 | if (message.showButton) { 86 | document.getElementById('show-code').style.display = 'inline'; 87 | document.getElementById('highlighted-code-status').classList.add('white-text'); 88 | } else { 89 | document.getElementById('show-code').style.display = 'none'; 90 | document.getElementById('highlighted-code-status').classList.remove('white-text'); 91 | } 92 | break; 93 | case "updateModelConfigDetails": 94 | document.getElementById('model-name').textContent = message.modelConfigDetails; 95 | break; 96 | case "updatePreferences": 97 | gPreferences = message.preferences; 98 | if (gPreferences.pressEnterToSend) { 99 | document.getElementById('send-button').textContent = "⏎ to Send"; 100 | document.getElementById('send-button').style.fontSize = '0.8em'; 101 | } else { 102 | console.log('unhiding send-button'); 103 | document.getElementById('send-button').style.fontSize = '1.1em'; 104 | document.getElementById('send-button').textContent = "Send"; 105 | } 106 | break; 107 | default: 108 | throw new Error(`Unknown command: ${message.command}`); 109 | } 110 | }); 111 | 112 | function addMessage(role, content, streaming = false) { 113 | let className; 114 | switch (role) { 115 | case "user": 116 | className = "user-message"; 117 | break; 118 | case "assistant": 119 | className = "assistant-message"; 120 | break; 121 | case "system": 122 | className = "system-message"; 123 | break; 124 | case "extension": 125 | className = "extension-message"; 126 | break; 127 | default: 128 | throw new Error(`Unknown role: ${role}`); 129 | } 130 | 131 | function isUserScrolledToBottom() { 132 | const thresholdPercentage = 2.5; 133 | const thresholdPixels = messagesContainer.clientHeight * (thresholdPercentage / 100); 134 | const distanceFromBottom = messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight; 135 | return distanceFromBottom < thresholdPixels; 136 | } 137 | const atBottomBeforeInsert = isUserScrolledToBottom(); 138 | 139 | let messageElement; 140 | 141 | if (streaming) { 142 | messageElement = document.querySelector(`.${role}-message:last-child`); 143 | if (!messageElement) { 144 | messageElement = document.createElement("div"); 145 | messageElement.className = className; 146 | } 147 | } else { 148 | messageElement = document.createElement("div"); 149 | messageElement.className = className; 150 | } 151 | 152 | messageElement.innerHTML = content; 153 | 154 | const codeBlocks = messageElement.querySelectorAll('pre code'); 155 | codeBlocks.forEach((codeBlock) => { 156 | const preElement = codeBlock.parentNode; 157 | if (preElement.tagName === 'PRE') { 158 | const wrapper = document.createElement('div'); 159 | wrapper.className = 'code-block-wrapper'; 160 | preElement.parentNode.insertBefore(wrapper, preElement); 161 | if (role === 'assistant') { 162 | const button = createCopyCodeButton(codeBlock); 163 | wrapper.appendChild(button); 164 | } 165 | wrapper.appendChild(preElement); 166 | } 167 | }); 168 | highlightCodeBlocks(messageElement); 169 | 170 | messagesContainer.insertAdjacentElement("beforeend", messageElement); 171 | 172 | if (role !== 'assistant' || atBottomBeforeInsert) { 173 | messagesContainer.scrollTop = messagesContainer.scrollHeight; 174 | } 175 | } 176 | 177 | function sendMessage() { 178 | const input = document.getElementById("message-input"); 179 | const userMessage = input.value; 180 | input.value = ""; 181 | autoResize(input); 182 | 183 | if (!userMessage) { 184 | return; 185 | } 186 | 187 | vscode.postMessage( 188 | { 189 | command: "getGptResponse", 190 | userMessage: escapeHtml(userMessage), 191 | } 192 | ); 193 | } 194 | }); 195 | 196 | function escapeHtml(html) { 197 | let inCodeBlock = false; 198 | let escapedHtml = ''; 199 | const codeBlockRegex = /(```|`)/g; 200 | const htmlEntities = [ 201 | { regex: /&/g, replacement: '&' }, 202 | { regex: //g, replacement: '>' }, 204 | { regex: /"/g, replacement: '"' }, 205 | { regex: /'/g, replacement: ''' }, 206 | ]; 207 | 208 | html.split(codeBlockRegex).forEach((segment, index) => { 209 | // If the index is even, it's not a code block. 210 | // If the index is odd, it's a code block. 211 | if (index % 2 === 0) { 212 | if (!inCodeBlock) { 213 | htmlEntities.forEach(({ regex, replacement }) => { 214 | segment = segment.replace(regex, replacement); 215 | }); 216 | } 217 | } else { 218 | inCodeBlock = !inCodeBlock; 219 | } 220 | escapedHtml += segment; 221 | }); 222 | 223 | return escapedHtml; 224 | } 225 | 226 | 227 | function autoResize(textarea) { 228 | textarea.style.height = 'auto'; 229 | textarea.style.height = textarea.scrollHeight + 'px'; 230 | } 231 | 232 | function handleTabInTextarea(textarea) { 233 | textarea.addEventListener('keydown', (e) => { 234 | if (e.key === 'Tab') { 235 | e.preventDefault(); 236 | const start = textarea.selectionStart; 237 | const end = textarea.selectionEnd; 238 | textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(end); 239 | textarea.selectionStart = textarea.selectionEnd = start + 1; 240 | } 241 | }); 242 | } 243 | 244 | function highlightCodeBlocks(element) { 245 | const codeBlocks = element.querySelectorAll('pre code'); 246 | codeBlocks.forEach((codeBlock) => { 247 | hljs.highlightBlock(codeBlock); 248 | }); 249 | } 250 | 251 | function createCopyCodeButton(codeBlock) { 252 | const COPY_BUTTON_TEXT = 'Copy code'; 253 | const button = document.createElement('button'); 254 | 255 | button.textContent = COPY_BUTTON_TEXT; 256 | button.className = 'copy-code-button'; 257 | button.addEventListener('click', () => { 258 | const selection = window.getSelection(); 259 | const range = document.createRange(); 260 | range.selectNodeContents(codeBlock); 261 | selection.removeAllRanges(); 262 | selection.addRange(range); 263 | 264 | try { 265 | // Some users on Linux reported that `navigator.clipboard.writeText` failed 266 | // Reluctantly using `document.execCommand('copy')` as a fallback 267 | document.execCommand('copy'); 268 | button.textContent = 'Copied!'; 269 | setTimeout(() => { 270 | button.textContent = COPY_BUTTON_TEXT; 271 | }, 2000); 272 | } catch (err) { 273 | console.error('Failed to copy text: ', err); 274 | } 275 | 276 | selection.removeAllRanges(); 277 | }); 278 | return button; 279 | } 280 | 281 | 282 | function wrapCodeBlocks(messageElement, role) { 283 | const codeBlocks = messageElement.querySelectorAll('pre code'); 284 | codeBlocks.forEach((codeBlock) => { 285 | const preElement = codeBlock.parentNode; 286 | if (preElement.tagName === 'PRE') { 287 | const wrapper = createCodeBlockWrapper(codeBlock, role); 288 | preElement.parentNode.insertBefore(wrapper, preElement); 289 | wrapper.appendChild(preElement); 290 | } 291 | }); 292 | } 293 | 294 | function createCodeBlockWrapper(codeBlock, role) { 295 | const wrapper = document.createElement('div'); 296 | wrapper.className = 'code-block-wrapper'; 297 | if (role === 'assistant') { 298 | // Future: enable drag n drop for code blocks 299 | // Must be configurable behavior (e.g. in settings) 300 | wrapper.draggable = false; 301 | wrapper.addEventListener('dragstart', (event) => { 302 | event.dataTransfer.setData('text/plain', codeBlock.textContent); 303 | }); 304 | const button = createCopyCodeButton(codeBlock); 305 | wrapper.appendChild(button); 306 | } 307 | return wrapper; 308 | } 309 | -------------------------------------------------------------------------------- /src/custom.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Configuration, OpenAIApi } from "openai"; 3 | import { APIProvider } from "./apiProvider"; 4 | import { OpenAIParams } from "./openai"; 5 | 6 | // Here we assume tha the custom LLM provider conform to the same API contract as OpenAI 7 | export class CustomLLMProvider extends APIProvider { 8 | private openaiCompatibleProvider: OpenAIApi | undefined; 9 | private context: vscode.ExtensionContext; 10 | private serverUrl: string|undefined; 11 | 12 | constructor(context: vscode.ExtensionContext, serverUrl: string|undefined) { 13 | super(); 14 | this.context = context; 15 | this.serverUrl = serverUrl; 16 | 17 | console.log(`Custom LLM provider initialized with base path: ${this.serverUrl}`); 18 | } 19 | 20 | async init() { 21 | // Get BasePath from the regular extension settings (not secret storage). If there isn't one, show an error message and return. 22 | if (!this.serverUrl || this.serverUrl === undefined) { 23 | vscode.window.showErrorMessage("No LLM base path configured. Please configure a base path and restart the extension."); 24 | return; 25 | } 26 | 27 | const configuration = new Configuration({ 28 | basePath: this.serverUrl, 29 | }); 30 | this.openaiCompatibleProvider = new OpenAIApi(configuration); 31 | } 32 | 33 | async completeStream(params: OpenAIParams, callbacks: any) { 34 | if (!this.openaiCompatibleProvider) { 35 | throw new Error("OpenAI API is not initialized."); 36 | } 37 | 38 | try { 39 | const res: any = await this.openaiCompatibleProvider.createChatCompletion(params, { responseType: 'stream' }); 40 | 41 | let buffer = ""; 42 | let gptMessage = ""; 43 | 44 | for await (const chunk of res.data) { 45 | 46 | buffer += chunk.toString("utf8"); 47 | const lines = buffer.split("\n"); 48 | buffer = lines.pop() || ""; 49 | for (const line of lines) { 50 | const message = line.replace(/^data: /, ""); 51 | if (message === "[DONE]") { 52 | if (callbacks.onComplete) { 53 | callbacks.onComplete(gptMessage); 54 | } 55 | return; 56 | } 57 | if (message.length === 0) { 58 | continue; 59 | } 60 | 61 | try { 62 | const json = JSON.parse(message); 63 | const token = json.choices[0].delta.content; 64 | if (token) { 65 | gptMessage += token; 66 | if (callbacks.onUpdate) { 67 | callbacks.onUpdate(gptMessage); 68 | } 69 | } 70 | } catch (error) { 71 | console.error("Error parsing message:", error); 72 | continue; 73 | } 74 | } 75 | } 76 | } catch (error: any) { 77 | console.error("Error fetching stream:", error); 78 | throw error; 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from 'vscode'; 4 | import * as path from 'path'; 5 | import * as marked from 'marked'; 6 | import * as fs from 'fs'; 7 | import * as os from 'os'; 8 | 9 | import { getProviderErrorMsg, promptForApiKey, providerFromModel } from './utils'; 10 | 11 | import { ChatCompletionRequestMessage } from "openai"; 12 | 13 | import { APIProvider } from "./apiProvider"; 14 | import { AnthropicProvider, AnthropicParams } from "./anthropic"; 15 | import { OpenAIProvider, OpenAIParams } from "./openai"; 16 | import { CustomLLMProvider } from './custom'; 17 | 18 | interface ResourcePaths { 19 | htmlPath: string; 20 | chatideJsPath: string; 21 | chatideCssPath: string; 22 | iconPath: string; 23 | highlightJsCssPath: string; 24 | highlightJsScriptPath: string; 25 | } 26 | 27 | interface Preferences { 28 | pressEnterToSend: false; 29 | autoSaveEnabled: true; 30 | }; 31 | 32 | console.log("Node.js version:", process.version); 33 | 34 | const isMac = process.platform === "darwin"; 35 | 36 | const OS_LOCALIZED_KEY_CHORD = isMac ? "Cmd+Shift+P" : "Ctrl+Shift+P"; 37 | const NO_SELECTION_COPY = "No code is highlighted. Highlight code to include it in the message to ChatGPT."; 38 | const SELECTION_AWARENESS_OFF_COPY = `Code selection awareness is turned off. To turn it on, go to settings (${OS_LOCALIZED_KEY_CHORD}).`; 39 | 40 | let apiProvider: APIProvider | undefined; 41 | let messages: ChatCompletionRequestMessage[] = []; 42 | 43 | let selectedCode: string; 44 | let selectedCodeSentToGpt: string; 45 | 46 | let highlightedCodeAwareness: boolean = vscode.workspace.getConfiguration('chatide').get('highlightedCodeAwareness') || false; 47 | let customServerUrl: string | undefined = vscode.workspace.getConfiguration('chatide').get('customServerUrl') || undefined; 48 | let pressEnterToSend: boolean = vscode.workspace.getConfiguration('chatide').get('pressEnterToSend') || false; 49 | let autoSaveEnabled: boolean = vscode.workspace.getConfiguration('chatide').get('autoSaveEnabled') || true; 50 | let currentSessionName: string|undefined = undefined; 51 | 52 | function gatherPreferences(): Preferences { 53 | const pressEnterToSend = vscode.workspace.getConfiguration('chatide').get('pressEnterToSend') || false; 54 | const autoSaveEnabled = vscode.workspace.getConfiguration('chatide').get('autoSaveEnabled') || false; 55 | return { 56 | pressEnterToSend, 57 | autoSaveEnabled 58 | } as Preferences; 59 | } 60 | 61 | // This method is called when your extension is activated 62 | // Your extension is activated the very first time the command is executed 63 | export function activate(context: vscode.ExtensionContext) { 64 | console.log("activate chatide"); 65 | 66 | context.subscriptions.push( 67 | vscode.commands.registerCommand('chatide.openSettings', openSettings) 68 | ); 69 | 70 | context.subscriptions.push( 71 | vscode.commands.registerCommand('chatide.updateOpenAiApiKey', async () => { 72 | await promptForApiKey("openAi", context); 73 | }) 74 | ); 75 | 76 | context.subscriptions.push( 77 | vscode.commands.registerCommand('chatide.updateAnthropicApiKey', async () => { 78 | await promptForApiKey("anthropic", context); 79 | }) 80 | ); 81 | 82 | const secretStorage = context.secrets; 83 | const secretChangeListener = secretStorage.onDidChange(async (e: vscode.SecretStorageChangeEvent) => { 84 | const forceReinit = true; 85 | 86 | if (e.key === "chatide.anthropicApiKey") { 87 | const key = await context.secrets.get("chatide.anthropicApiKey"); 88 | if (!key) { 89 | return; 90 | } 91 | // Reinitialize the API provider if the Anthropic API key changes 92 | await initApiProviderIfNeeded(context, forceReinit); 93 | } else if (e.key === "chatide.openAiApiKey") { 94 | const key = await context.secrets.get("chatide.openAiApiKey"); 95 | if (!key) { 96 | return; 97 | } 98 | // Reinitialize the API provider if the OpenAI API key changes 99 | await initApiProviderIfNeeded(context, forceReinit); 100 | } 101 | }); 102 | 103 | context.subscriptions.push(secretChangeListener); 104 | 105 | let disposable = vscode.commands.registerCommand('chatide.start', async () => { 106 | const chatIdePanel = vscode.window.createWebviewPanel( 107 | 'chatIde', 108 | 'ChatIDE', 109 | vscode.ViewColumn.Beside, 110 | { 111 | // allow the extension to reach files in the bundle 112 | localResourceRoots: [vscode.Uri.file(path.join(__dirname, '..'))], 113 | enableScripts: true, 114 | // Retain the context when the webview becomes hidden 115 | retainContextWhenHidden: true, 116 | }, 117 | ); 118 | 119 | const htmlPathUri = vscode.Uri.file(path.join(context.extensionPath, 'src', 'chatide.html')); 120 | const htmlPath = htmlPathUri.with({ scheme: 'vscode-resource' }); 121 | 122 | let jsPathUri = vscode.Uri.file(context.asAbsolutePath(path.join('src', "chatide.js"))); 123 | const jsPath = chatIdePanel.webview.asWebviewUri(jsPathUri).toString(); 124 | 125 | let cssUri = vscode.Uri.file(context.asAbsolutePath(path.join('src', "chatide.css"))); 126 | const cssPath = chatIdePanel.webview.asWebviewUri(cssUri).toString(); 127 | 128 | let highlightJsCssUri = vscode.Uri.file(context.asAbsolutePath(path.join('src', "atom-one-dark.min.css"))); 129 | const highlightJsCssPath = chatIdePanel.webview.asWebviewUri(highlightJsCssUri).toString(); 130 | 131 | let highlightJsScriptUri = vscode.Uri.file(context.asAbsolutePath(path.join('src', "highlight.min.js"))); 132 | const highlightJsScriptPath = chatIdePanel.webview.asWebviewUri(highlightJsScriptUri).toString(); 133 | 134 | let iconUri = vscode.Uri.file(context.asAbsolutePath(path.join('assets', "icon.jpg"))); 135 | const iconPath = chatIdePanel.webview.asWebviewUri(iconUri).toString(); 136 | 137 | const model = vscode.workspace.getConfiguration('chatide').get('model') || "No model configured"; 138 | const provider = providerFromModel(model.toString()); 139 | 140 | const errorCallback = (error: any) => { 141 | console.error('Error fetching stream:', error); 142 | const errorMessage = error.message; 143 | const humanRedableError = getProviderErrorMsg(provider.toString(), errorMessage); 144 | chatIdePanel.webview.postMessage({ command: "openAiError", error: humanRedableError }); 145 | }; 146 | 147 | const configDetails = model.toString(); 148 | const resourcePaths = { 149 | htmlPath: htmlPath.fsPath, 150 | chatideJsPath: jsPath.toString(), 151 | chatideCssPath: cssPath.toString(), 152 | iconPath: iconPath.toString(), 153 | highlightJsCssPath: highlightJsCssPath, 154 | highlightJsScriptPath: highlightJsScriptPath, 155 | }; 156 | 157 | chatIdePanel.webview.html = getWebviewContent(resourcePaths, configDetails); 158 | 159 | const preferences = gatherPreferences(); 160 | console.log("preferences", preferences); 161 | chatIdePanel.webview.postMessage({ 162 | command: 'updatePreferences', 163 | preferences 164 | }); 165 | 166 | resetChat(); 167 | 168 | chatIdePanel.webview.onDidReceiveMessage(async (message) => { 169 | switch (message.command) { 170 | case "getGptResponse": 171 | // Turn the user's message to Markdown and echo it back 172 | const userMessageMarkdown = marked.marked(message.userMessage); 173 | chatIdePanel.webview.postMessage({ command: "sentUserMessage", userMessageMarkdown }); 174 | 175 | // Proceed to query OpenAI API and stream back the generated tokens. 176 | await initApiProviderIfNeeded(context); 177 | 178 | await getGptResponse( 179 | message.userMessage, 180 | (token) => { 181 | chatIdePanel.webview.postMessage({ command: "gptResponse", token }); 182 | }, 183 | errorCallback 184 | ); 185 | return; 186 | case "resetChat": 187 | resetChat(); 188 | chatIdePanel.webview.postMessage({ command: "resetChatComplete" }); 189 | return; 190 | case "exportChat": 191 | await exportChat(); 192 | return; 193 | case "importChat": 194 | const success = await importChat(); 195 | if (success) { 196 | chatIdePanel.webview.postMessage({ command: "loadChatComplete", messages }); 197 | } else { 198 | console.error("Failed to import chat"); 199 | } 200 | return; 201 | case "navigateToHighlightedCode": 202 | navigateToHighlightedCode(); 203 | return; 204 | case "insertCode": // used for drag and drop 205 | const activeEditor = vscode.window.activeTextEditor; 206 | if (activeEditor) { 207 | const position = activeEditor.selection.active; 208 | activeEditor.edit((editBuilder) => { 209 | editBuilder.insert(position, message.code); 210 | }); 211 | } 212 | return; 213 | case "openSettings": 214 | vscode.commands.executeCommand('workbench.action.openSettings', 'chatide'); 215 | break; 216 | case "toggleAutoSave": 217 | autoSaveEnabled = message.enabled; 218 | break; 219 | } 220 | }, 221 | null, 222 | context.subscriptions 223 | ); 224 | 225 | // Add an event listener for selection changes 226 | context.subscriptions.push( 227 | vscode.window.onDidChangeTextEditorSelection((event) => handleSelectionChange(event, chatIdePanel)) 228 | ); 229 | 230 | // listen for changes in highlightedCodeAwareness 231 | vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => { 232 | if (e.affectsConfiguration('chatide.highlightedCodeAwareness')) { 233 | highlightedCodeAwareness = vscode.workspace.getConfiguration('chatide').get('highlightedCodeAwareness') || false; 234 | 235 | // This is imperfect because if there's code selected while the setting is changed 236 | // the status copy will be 'wrong'. 237 | chatIdePanel.webview.postMessage({ 238 | command: 'updateHighlightedCodeStatus', 239 | status: !highlightedCodeAwareness ? SELECTION_AWARENESS_OFF_COPY : NO_SELECTION_COPY, 240 | showButton: false 241 | }); 242 | } 243 | if (e.affectsConfiguration('chatide.pressEnterToSend')) { 244 | console.log(`pressEnterToSend changed to ${vscode.workspace.getConfiguration('chatide').get('pressEnterToSend')}`); 245 | pressEnterToSend = vscode.workspace.getConfiguration('chatide').get('pressEnterToSend') || false; 246 | chatIdePanel.webview.postMessage({ 247 | command: 'updatePreferences', 248 | preferences: gatherPreferences(), 249 | }); 250 | } 251 | 252 | if (e.affectsConfiguration('chatide.customServerUrl')) { 253 | console.log(`customServerUrl changed to ${vscode.workspace.getConfiguration('chatide').get('customServerUrl')}`); 254 | initApiProviderIfNeeded(context, true); 255 | } 256 | 257 | if (e.affectsConfiguration('chatide.model')) { 258 | initApiProviderIfNeeded(context, true); 259 | chatIdePanel.webview.postMessage({ 260 | command: 'updateModelConfigDetails', 261 | modelConfigDetails: vscode.workspace.getConfiguration('chatide').get('model')!, 262 | }); 263 | } 264 | }); 265 | 266 | chatIdePanel.onDidDispose( 267 | () => { 268 | console.log('WebView closed'); 269 | }, 270 | null, 271 | context.subscriptions 272 | ); 273 | }); 274 | 275 | context.subscriptions.push(disposable); 276 | } 277 | 278 | function openSettings() { 279 | vscode.commands.executeCommand('workbench.action.openSettings', 'chatide'); 280 | } 281 | 282 | function getWebviewContent( 283 | paths: ResourcePaths, 284 | modelConfigDetails: string) { 285 | const codeHighlightStatusCopy = !highlightedCodeAwareness ? SELECTION_AWARENESS_OFF_COPY : NO_SELECTION_COPY; 286 | 287 | console.log(`Loading webview content from ${paths.htmlPath}`); 288 | 289 | const html = fs.readFileSync(paths.htmlPath, 'utf8'); 290 | const variables = { 291 | paths, 292 | modelConfigDetails, 293 | codeHighlightStatusCopy, 294 | autoSaveEnabled 295 | }; 296 | 297 | const webviewHtml = (new Function("variables", `with (variables) { return \`${html}\`; }`))(variables); 298 | 299 | return webviewHtml; 300 | } 301 | 302 | function resetChat() { 303 | console.log("Resetting chat"); 304 | // Load the sytem prompt and clear the chat history. 305 | let systemPrompt: any = vscode.workspace.getConfiguration('chatide').get('systemPrompt'); 306 | if (!systemPrompt) { 307 | vscode.window.showErrorMessage('No system prompt found in the ChatIDE settings. Please add your system prompt using the "Open ChatIDE Settings" command and restart the extension.'); 308 | return; 309 | } 310 | 311 | currentSessionName = undefined; 312 | messages = []; 313 | messages.push({ "role": "system", "content": systemPrompt.toString() }); 314 | } 315 | 316 | async function getGptResponse(userMessage: string, completionCallback: (completion: string) => void, errorCallback?: (error: any) => void) { 317 | if (!apiProvider) { 318 | throw new Error("API provider is not initialized."); 319 | } 320 | 321 | if (highlightedCodeAwareness && selectedCodeSentToGpt !== selectedCode) { 322 | console.log("Including highlighted text in API request."); 323 | userMessage = `${prepareSelectedCodeContext()} ${userMessage}`; 324 | selectedCodeSentToGpt = selectedCode; 325 | } else { 326 | console.log("Not including highlighted text in API request because it's already been sent"); 327 | } 328 | 329 | messages.push({ role: "user", content: userMessage }); 330 | 331 | const maxTokens = vscode.workspace.getConfiguration("chatide").get("maxLength"); 332 | const model = vscode.workspace.getConfiguration("chatide").get("model")!; 333 | let provider = providerFromModel(model.toString()); 334 | const temperature = vscode.workspace.getConfiguration("chatide").get("temperature"); 335 | 336 | if (!maxTokens) { 337 | vscode.window.showErrorMessage( 338 | 'Missing maxLength in the ChatIDE settings. Please add them using the "Open ChatIDE Settings" command and restart the extension.' 339 | ); 340 | return; 341 | } 342 | 343 | let params: OpenAIParams | AnthropicParams; 344 | 345 | if (provider === "openai" || provider === "custom") { 346 | params = { 347 | model: model.toString(), 348 | messages: messages, 349 | // eslint-disable-next-line @typescript-eslint/naming-convention 350 | max_tokens: Number(maxTokens), 351 | temperature: Number(temperature), 352 | stream: true, 353 | }; 354 | } else if (provider === "anthropic") { 355 | params = { 356 | messages, 357 | // eslint-disable-next-line @typescript-eslint/naming-convention 358 | max_tokens: Number(maxTokens), 359 | model: model.toString(), 360 | temperature: Number(temperature), 361 | stream: true 362 | }; 363 | } 364 | else { 365 | vscode.window.showErrorMessage( 366 | 'Unsupported AI provider in the ChatIDE settings. Please add it using the "Open ChatIDE Settings" command and restart the extension.' 367 | ); 368 | return; 369 | } 370 | 371 | try { 372 | await apiProvider.completeStream( 373 | params, 374 | { 375 | onUpdate: (completion: string) => { 376 | if (completion) { 377 | completionCallback(marked.marked(completion ?? "")); 378 | } 379 | }, 380 | onComplete: (message: string) => { 381 | messages.push({ "role": "assistant", "content": message }); 382 | autoSaveMessages(); // Add this line to auto-save messages 383 | } 384 | } 385 | ); 386 | } catch (error: any) { 387 | if (errorCallback) { 388 | errorCallback(error); 389 | } 390 | } 391 | } 392 | 393 | 394 | async function autoSaveMessages() { 395 | if (!autoSaveEnabled) { 396 | return; 397 | } 398 | 399 | const autoSaveDirectory = vscode.workspace.getConfiguration('chatide').get('autoSaveDirectory') as string; 400 | if (!autoSaveDirectory) { 401 | return; 402 | } 403 | 404 | // Resolve the full path using os.homedir() 405 | const fullPath = autoSaveDirectory.startsWith('~') 406 | ? path.join(os.homedir(), autoSaveDirectory.slice(1)) 407 | : autoSaveDirectory; 408 | 409 | // Create the directory if it doesn't exist 410 | if (!fs.existsSync(fullPath)) { 411 | fs.mkdirSync(fullPath, { recursive: true }); 412 | } 413 | 414 | if (!currentSessionName) { 415 | const timestamp = new Date().toISOString().replace(/:/g, '-'); 416 | currentSessionName = `chatide-chat-${timestamp}.json`; 417 | } 418 | 419 | const filePath = path.join(fullPath, currentSessionName); 420 | 421 | const content = JSON.stringify(messages, null, 2); 422 | fs.writeFile(filePath, content, (err) => { 423 | if (err) { 424 | console.error('Failed to auto-save messages:', err); 425 | } else { 426 | console.log('Messages auto-saved successfully!'); 427 | } 428 | }); 429 | } 430 | 431 | async function exportChat() { 432 | const options: vscode.SaveDialogOptions = { 433 | defaultUri: vscode.Uri.file('chatIDE-history-'), 434 | filters: { 435 | // eslint-disable-next-line @typescript-eslint/naming-convention 436 | 'JSON': ['json'] 437 | } 438 | }; 439 | 440 | const fileUri = await vscode.window.showSaveDialog(options); 441 | if (fileUri) { 442 | const content = JSON.stringify(messages, null, 2); 443 | fs.writeFile(fileUri.fsPath, content, (err) => { 444 | if (err) { 445 | vscode.window.showErrorMessage('Failed to export messages: ' + err.message); 446 | } else { 447 | vscode.window.showInformationMessage('Messages exported successfully!'); 448 | } 449 | }); 450 | } 451 | } 452 | 453 | // Import chat history from a JSON file 454 | async function importChat() { 455 | const options: vscode.OpenDialogOptions = { 456 | canSelectMany: false, 457 | filters: { 458 | // eslint-disable-next-line @typescript-eslint/naming-convention 459 | 'Chat History': ['json'] 460 | } 461 | }; 462 | 463 | const fileUri = await vscode.window.showOpenDialog(options); 464 | if (fileUri && fileUri[0]) { 465 | try { 466 | const data = await fs.promises.readFile(fileUri[0].fsPath, 'utf8'); 467 | const importedMessages = JSON.parse(data); 468 | 469 | messages = importedMessages.map((message: any) => { 470 | return { 471 | "role": message.role, 472 | "content": marked.marked(message.content) 473 | }; 474 | }); 475 | vscode.window.showInformationMessage('Messages imported successfully!'); 476 | return true; 477 | } catch (e: any) { 478 | if (e.code === 'ENOENT') { 479 | vscode.window.showErrorMessage('Failed to import messages: ' + e.message); 480 | } else { 481 | vscode.window.showErrorMessage('Failed to parse JSON: ' + e.message); 482 | } 483 | } 484 | } 485 | 486 | return false; 487 | } 488 | 489 | async function initApiProviderIfNeeded(context: vscode.ExtensionContext, force: boolean = false) { 490 | console.log("Initializing API provider..."); 491 | if (apiProvider !== undefined && !force) { 492 | console.log("API provider already initialized."); 493 | return; 494 | } 495 | 496 | const model = vscode.workspace.getConfiguration("chatide").get("model")!; 497 | const providerType = providerFromModel(model.toString()); 498 | if (!providerType) { 499 | vscode.window.showErrorMessage( 500 | 'No provider found in the ChatIDE settings. Please add your provider using the "Open ChatIDE Settings" command and restart the extension.' 501 | ); 502 | return; 503 | } 504 | 505 | if (providerType === "anthropic") { 506 | console.log("Initializing Anthropic provider..."); 507 | apiProvider = new AnthropicProvider(context); 508 | } else if (providerType === "openai") { 509 | console.log("Initializing OpenAI provider..."); 510 | apiProvider = new OpenAIProvider(context); 511 | } 512 | else if (providerType === "custom") { 513 | console.log("Initializing custom provider..."); 514 | apiProvider = new CustomLLMProvider(context, customServerUrl); 515 | } else { 516 | vscode.window.showErrorMessage( 517 | `Invalid provider "${providerType}" in the ChatIDE settings. Please use a valid provider and restart the extension.` 518 | ); 519 | return; 520 | } 521 | 522 | try { 523 | console.log("Calling init()"); 524 | await apiProvider.init(); 525 | console.log("init() returned."); 526 | } catch (error: any) { 527 | vscode.window.showErrorMessage(`Error initializing provider: ${error.message}`); 528 | } 529 | } 530 | 531 | function navigateToHighlightedCode() { 532 | const editor = vscode.window.activeTextEditor; 533 | if (!editor) { 534 | return; 535 | } 536 | 537 | const selection = editor.selection; 538 | if (!selection.isEmpty) { 539 | editor.revealRange(selection, vscode.TextEditorRevealType.Default); 540 | } 541 | } 542 | 543 | function getTokenEstimateString(numCharacters: number): string { 544 | const estimate = Math.round(numCharacters / 4); 545 | if (estimate === 1) { 546 | return `~${estimate} token`; 547 | } 548 | return `~${estimate} tokens`; 549 | } 550 | 551 | function handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent, chatIdePanel: vscode.WebviewPanel) { 552 | selectedCode = event.textEditor.document.getText(event.selections[0]); 553 | if (selectedCode && highlightedCodeAwareness) { 554 | const numCharacters = selectedCode.length; 555 | chatIdePanel.webview.postMessage({ 556 | command: 'updateHighlightedCodeStatus', 557 | status: `${numCharacters} characters (${getTokenEstimateString(numCharacters)}) are highlighted. This code will be included in your message to the assistant.`, 558 | showButton: true 559 | }); 560 | } else if (!highlightedCodeAwareness) { 561 | chatIdePanel.webview.postMessage({ 562 | command: 'updateHighlightedCodeStatus', 563 | status: SELECTION_AWARENESS_OFF_COPY, 564 | showButton: false 565 | }); 566 | } else { 567 | chatIdePanel.webview.postMessage({ 568 | command: 'updateHighlightedCodeStatus', 569 | status: NO_SELECTION_COPY, 570 | showButton: false 571 | }); 572 | } 573 | } 574 | 575 | function prepareSelectedCodeContext() { 576 | return ` 577 | CONTEXT: 578 | ========================= 579 | In my question I am referring to the following code: 580 | ${selectedCode} 581 | =========================\n`; 582 | } 583 | 584 | // This method is called when your extension is deactivated 585 | export function deactivate() { 586 | console.log("deactivate chatide"); 587 | } 588 | -------------------------------------------------------------------------------- /src/openai.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Configuration, OpenAIApi } from "openai"; 3 | import { APIProvider } from "./apiProvider"; 4 | 5 | export interface OpenAIParams { 6 | model: string; 7 | messages: any[]; 8 | // eslint-disable-next-line @typescript-eslint/naming-convention 9 | max_tokens: number; 10 | temperature: number; 11 | stream: boolean; 12 | } 13 | 14 | export class OpenAIProvider extends APIProvider { 15 | private openai: OpenAIApi | undefined; 16 | private context: vscode.ExtensionContext; 17 | 18 | constructor(context: vscode.ExtensionContext) { 19 | super(); 20 | this.context = context; 21 | } 22 | 23 | async init() { 24 | let openAiApiKey = await this.context.secrets.get("chatide.openAiApiKey"); 25 | 26 | if (!openAiApiKey) { 27 | openAiApiKey = await vscode.window.showInputBox({ 28 | prompt: "Enter your OpenAI API key:", 29 | ignoreFocusOut: true, 30 | }); 31 | if (openAiApiKey) { 32 | await this.context.secrets.store("chatide.openAiApiKey", openAiApiKey); 33 | } else { 34 | throw new Error("No API key provided. Please add your API key and restart the extension."); 35 | } 36 | } 37 | 38 | const configuration = new Configuration({ 39 | apiKey: openAiApiKey, 40 | }); 41 | this.openai = new OpenAIApi(configuration); 42 | } 43 | 44 | async completeStream(params: OpenAIParams, callbacks: any) { 45 | if (!this.openai) { 46 | throw new Error("OpenAI API is not initialized."); 47 | } 48 | 49 | try { 50 | const res: any = await this.openai.createChatCompletion(params, { responseType: 'stream' }); 51 | 52 | let buffer = ""; 53 | let gptMessage = ""; 54 | 55 | for await (const chunk of res.data) { 56 | 57 | buffer += chunk.toString("utf8"); 58 | const lines = buffer.split("\n"); 59 | buffer = lines.pop() || ""; 60 | for (const line of lines) { 61 | const message = line.replace(/^data: /, ""); 62 | if (message === "[DONE]") { 63 | if (callbacks.onComplete) { 64 | callbacks.onComplete(gptMessage); 65 | } 66 | return; 67 | } 68 | if (message.length === 0) { 69 | continue; 70 | } 71 | 72 | try { 73 | const json = JSON.parse(message); 74 | const token = json.choices[0].delta.content; 75 | if (token) { 76 | gptMessage += token; 77 | if (callbacks.onUpdate) { 78 | callbacks.onUpdate(gptMessage); 79 | } 80 | } 81 | } catch (error) { 82 | console.error("Error parsing message:", error); 83 | continue; 84 | } 85 | } 86 | } 87 | } catch (error: any) { 88 | console.error("Error fetching stream:", error); 89 | throw error; 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests', err); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | const supportedProviders = ["openai", "anthropic"]; 4 | 5 | export function getProviderErrorMsg(provider: string, error: any) { 6 | const model = vscode.workspace.getConfiguration('chatide').get('model') || "No model configured"; 7 | const epilogue = ` 8 | \t • Invalid API Key: make sure you entered it correctly (Need help? See Setting your AI provider API key).
9 | \t • Invalid Model name: make sure you chose a supported model. Your current model is ${model.toString()}
10 | \t • Model not compatible with your API Key: your key might not grant you access to this model.
11 | \t • Chat history too long: models have a limited context window. Export your current history to file and start a new chat.

12 | Double check your configuration and restart VS Code to try again.

13 | If the issue persists, please open an issue on GitHub or contact us on Twitter. 14 | `; 15 | 16 | if (provider === "openai") { 17 | return ` 18 | You're hitting an OpenAI API error.

19 | Error message: '${error}'.

20 | Common reasons for OpenAI errors:

21 | \t • OpenAI might be having issues: check the OpenAI system status page.
22 | \t • Exceeded quota: make sure your OpenAI billing is setup correctly.
23 | ${epilogue} 24 | `; 25 | } else if (provider === "anthropic") { 26 | return ` 27 | You're hitting an Anthropic API error.

28 | Error message: '${error}'.

29 | Common reasons for Anthropic errors:

30 | \t • Anthropic might be having issues: check Anthropic's twitter page.
31 | \t • Exceeded quota: make sure your Anthropic billing is setup correctly.
32 | ${epilogue} 33 | `; 34 | } 35 | 36 | return `Error: ${error}`; 37 | } 38 | 39 | export async function promptForApiKey(provider: string, context: vscode.ExtensionContext) { 40 | if (!supportedProviders.includes(provider.toLowerCase())) { 41 | vscode.window.showErrorMessage(`Invalid provider "${provider}" in the ChatIDE settings. Please use a valid provider and restart the extension.`); 42 | return; 43 | } 44 | 45 | let providerCleanName = provider.charAt(0).toUpperCase() + provider.slice(1); 46 | 47 | const apiKey = await vscode.window.showInputBox({ 48 | prompt: `Enter your ${providerCleanName} API key to use ChatIDE. Your API key will be stored in VS Code\'s SecretStorage.`, 49 | ignoreFocusOut: true, 50 | password: true, 51 | }); 52 | 53 | const secretStorageKey = `chatide.${provider}ApiKey`; 54 | 55 | if (apiKey) { 56 | await context.secrets.store(secretStorageKey, apiKey); 57 | vscode.window.showInformationMessage(`API key stored successfully (under '${secretStorageKey}').`); 58 | } else { 59 | vscode.window.showErrorMessage('No API key entered. Please enter your API key to use ChatIDE.'); 60 | } 61 | } 62 | 63 | export function providerFromModel(model: string) { 64 | if (model.startsWith("gpt")) { 65 | return "openai"; 66 | } 67 | if (model === "custom") { 68 | return "custom"; 69 | } 70 | return "anthropic"; 71 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 37 | * Press `F5` to run the tests in a new window with your extension loaded. 38 | * See the output of the test result in the debug console. 39 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 40 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 41 | * You can create folders inside the `test` folder to structure your tests any way you want. 42 | 43 | ## Go further 44 | 45 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 46 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 47 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 48 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | 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/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | devtool: 'nosources-source-map', 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [ extensionConfig ]; --------------------------------------------------------------------------------