Compare commits

...

121 Commits

Author SHA1 Message Date
mp ffab60b4b8 Merge branch 'main' into diff-selections 2024-10-14 21:48:34 -07:00
mp 2309fc53fb Merge dynamic diff features 2024-10-14 20:37:45 -07:00
Andrew Pareles e153ffee4e Merge pull request #51 from okxiaoliang4/feature/voidrules
feat: system prompt `.voidrules`
2024-10-14 20:29:54 -07:00
Andrew a3d741d685 Merge branch 'main' into pr/okxiaoliang4/51 2024-10-14 20:24:38 -07:00
Andrew a12e35c5f0 merge package.json 2024-10-14 20:14:44 -07:00
Andrew 5f3e664eed delete ctrl+K placeholder 2024-10-14 18:12:11 -07:00
Andrew fb69afd5c4 fix thread history persistence 2024-10-14 18:07:22 -07:00
Andrew Pareles 8c9eeaa133 Merge pull request #98 from ShikangPang/openai_compatible
fix: Refactor openAICompatible initialization in sendLLMMessage.ts
2024-10-14 16:45:49 -07:00
Andrew 00f59e9555 Merge branch 'main' into pr/ShikangPang/98 2024-10-14 16:45:31 -07:00
Andrew 767c45080f update openai 2024-10-14 16:44:57 -07:00
Andrew Pareles b0758e02aa Merge pull request #99 from IDLe-Engineering/patch-1
Update sendLLMMessage.ts
2024-10-14 16:21:03 -07:00
Tom Spielvogel 7f81836d9f Update sendLLMMessage.ts
In order to take the enpoint specified for the OpenAICompatible setting - the openai class needs to be instanciated with the baseURL setting from apiConfig
2024-10-14 16:18:41 +02:00
Mathew P 33c3ef6751 Fix newline bug 2024-10-13 22:05:10 -07:00
Mathew P 20056f209e Fix accept/reject diff 2024-10-13 17:47:13 -07:00
Andrew 69d848f22b update MarkdownRender 2024-10-13 17:11:29 -07:00
Mathew P 1557a44934 Better diff algorithm 2024-10-13 00:15:41 -07:00
pangshikang 89dd63a0ee fix: Refactor openAICompatible initialization in sendLLMMessage.ts 2024-10-12 16:03:29 +08:00
Andrew e5c55145e9 update selectedFiles display 2024-10-11 23:18:23 -07:00
Andrew 2638d62a24 only start a new thread if empty 2024-10-11 20:05:10 -07:00
Andrew Pareles e04d839640 Merge pull request #53 from anetaj/feature/persist-threads-history
Persist chat threads history
2024-10-11 19:19:13 -07:00
Andrew 0158e2fd5d fix ref issues with useInstantState 2024-10-11 19:18:28 -07:00
Andrew 3e308f2082 state is broken 2024-10-11 18:05:01 -07:00
Andrew 429224993b Merge branch 'main' into pr/anetaj/53 2024-10-11 17:15:52 -07:00
Andrew Pareles 1b8271d217 Merge pull request #52 from anetaj/feature/style-include-files
Input box style
2024-10-11 17:12:18 -07:00
Andrew 29ef6b5b43 finish merge from 'main' 2024-10-11 17:11:44 -07:00
Andrew 23a42659b3 Merge branch 'main' into pr/anetaj/52 2024-10-11 17:09:19 -07:00
Andrew 9d7a5eccd3 Merge branch 'main' into pr/anetaj/52 2024-10-11 16:58:55 -07:00
anetaj 42bdcf54ca Copy code block (#47)
* rearrange markdown components

* add copy button

* add default style for small button

* hide entire toolbar in user messages

* finish merge

* refactor

---------

Co-authored-by: Andrew <andrewpareles@gmail.com>
2024-10-11 16:56:44 -07:00
Andrew 251b65d04d finish merge 2024-10-11 16:35:33 -07:00
Andrew b72da01632 Merge branch 'main' into pr/anetaj/53 2024-10-11 16:35:09 -07:00
Andrew e2f337f882 OpenAICompatible improvements + misc refactor 2024-10-11 15:02:43 -07:00
Mathew P 496c0178de Prepare to fix diff highlighting 2024-10-11 14:03:11 -07:00
Mathew P 38d31ae737 Make diffs dynamic on changes 2024-10-11 01:06:17 -07:00
Andrew Pareles d3beafec74 Merge pull request #95 from jack1582/main
Supports calling LLM in a way compatible with OpenAI's API, and adds explicit configuration items for users。
2024-10-11 00:37:36 -07:00
Andrew Pareles 73fb5bb3c6 Merge branch 'main' into main 2024-10-11 00:36:43 -07:00
Andrew Pareles 247a3b7feb Merge pull request #92 from w1gs/ide-fixes
fixed dependencies that were preventing IDE builds
2024-10-11 00:22:46 -07:00
jackxiong 2e5be73f0c Supports calling LLM in a way compatible with OpenAI's API, and adds explicit configuration items for users。 2024-10-11 14:58:24 +08:00
Darion 2ca1db35af Merge branch 'main' into ide-fixes 2024-10-07 10:23:48 -04:00
Mathew P 11baebdf54 diffArea works. Need to fix highlighting 2024-10-07 01:56:29 -07:00
Andrew Pareles 8dde88222c add Python requirement in contrib 2024-10-06 21:21:59 -07:00
Andrew Pareles 2b47e9745f Merge pull request #91 from jcommaret/macos-build
MacOS IDE build
2024-10-06 20:31:18 -07:00
w1gs f96416be38 fixed dependencies that were preventing IDE builds 2024-10-06 00:17:15 -04:00
Jérôme Commaret a4ef0de0ac MacOS Full IDE ARM 2024-10-06 01:50:01 +02:00
Aneta Jastrzębska f7a905e27f Merge branch 'main' into feature/style-include-files 2024-10-05 11:57:04 +02:00
Andrew c579bcc2fd rm "void" 2024-10-04 22:27:33 -07:00
Andrew Pareles ad7b7a6866 Update CONTRIBUTING.md 2024-10-04 22:26:24 -07:00
Mathew P b07842ec3f Remove _computedLensesOfDocument 2024-10-03 03:01:27 -07:00
Mathew P 19b2b8bd77 create diffArea; remove _computedLensesOfDocument 2024-10-03 03:00:55 -07:00
Mathew P f4a407d80f Refactor and prepare to create addNewChanges 2024-10-03 00:33:17 -07:00
Andrew 95be6e0b37 update config provider string and prompt 2024-10-03 00:02:18 -07:00
Andrew 6741a6fceb update description 2024-10-02 23:25:42 -07:00
Andrew db560744fb update defaults, credit @okxiaoliang4 for package.json enum changes 2024-10-02 23:23:07 -07:00
Andrew Pareles 8937b52e72 Merge pull request #84 from voideditor/config-improvements
Improve Void VS Code configuration
2024-10-02 00:02:56 -07:00
Andrew 7550b96389 update settings 2024-10-02 00:00:04 -07:00
Andrew Pareles 8aa3282c3d Update CONTRIBUTING.md 2024-10-01 23:24:49 -07:00
Andrew Pareles 9ed68d5fac Update CONTRIBUTING.md 2024-10-01 23:24:19 -07:00
Andrew Pareles 7668d3dee7 Update CONTRIBUTING.md 2024-10-01 23:23:31 -07:00
Andrew Pareles fc65d847c4 Update CONTRIBUTING.md 2024-10-01 23:08:38 -07:00
Andrew Pareles 653d5e9c1f Merge pull request #22 from w1gs/main
Added Ollama integration
2024-10-01 22:26:07 -07:00
Andrew 631f724138 merge ollama 2024-10-01 22:25:16 -07:00
Andrew Pareles 162bf704cc Update CONTRIBUTING.md 2024-10-01 21:40:39 -07:00
Andrew d64e9c93f5 Chore : change "yarn" to "npm i" inside documentation #79 2024-10-01 16:25:31 -07:00
w1gs 74c610632e Merge remote-tracking branch 'upstream/main' 2024-09-30 23:21:08 -04:00
Aneta Jastrzębska 2496a2c42e Merge branch 'main' into feature/persist-threads-history 2024-09-30 11:34:28 +02:00
Andrew Pareles 656b24b701 Add PR guidelines to CONTRIBUTING.md 2024-09-30 01:39:42 -07:00
Andrew Pareles 3fd9b572fe Merge pull request #42 from DaveHogan/main
Minor improvement to whichapi & anthropic configuration
2024-09-30 00:54:11 -07:00
Andrew Pareles dab75a6614 Merge branch 'main' into main 2024-09-30 00:54:02 -07:00
Mathew Pareles 4747691e74 add enum 2024-09-30 00:53:07 -07:00
Andrew Pareles 6dac9626c0 Merge pull request #66 from voideditor/rebase
Rebase vscode into Void
2024-09-30 00:39:16 -07:00
Andrew Pareles 180df75bb4 Merge pull request #61 from anetaj/feature/open-settings-button
Link to settings from extension
2024-09-30 00:33:58 -07:00
Darion 5039ca8fdf Merge branch 'voideditor:main' into main 2024-09-24 02:05:10 -04:00
Andrew Pareles aea35c95f2 fix grammar 2024-09-23 22:34:53 -07:00
Aneta Jastrzębska 0a5ebc1c50 add settings button in ext 2024-09-23 12:02:12 +02:00
Dave Hogan cbe3d4bd45 Removes unnessary default 2024-09-22 17:36:53 +01:00
Dave Hogan 16b84ec933 Merge branch 'main' of https://github.com/DaveHogan/void 2024-09-22 17:35:02 +01:00
Dave Hogan 2661f5e8e1 Implemented feedback, removing explict defaults as this comes from package.json 2024-09-22 17:34:48 +01:00
Dave Hogan 760f8c447c Merge branch 'voideditor:main' into main 2024-09-22 17:22:17 +01:00
Darion 57db928f7a Merge branch 'voideditor:main' into main 2024-09-22 08:11:43 -04:00
Jelf 9b130edcfe feat: read .voidrules for system prompt 2024-09-22 17:35:51 +08:00
Aneta Jastrzębska ff0dcd5095 handle show thread history 2024-09-22 11:04:28 +02:00
Aneta Jastrzębska 30b528402c add new toolbar + commands 2024-09-22 11:03:42 +02:00
Jelf 6d513a1e60 chore: improve setting 2024-09-22 16:00:08 +08:00
Aneta Jastrzębska 7269c4e582 show new thread button 2024-09-21 21:19:36 +02:00
Aneta Jastrzębska 083bbf7d9f start new chat; toggle thread history 2024-09-21 21:11:52 +02:00
Aneta Jastrzębska e54f49ebfb threads history list 2024-09-21 19:19:26 +02:00
Aneta Jastrzębska 31cd10b9e6 handle receive threads history 2024-09-21 19:19:16 +02:00
Aneta Jastrzębska c6dcb8eb9e add message to history & sync saved thread 2024-09-21 18:07:07 +02:00
Aneta Jastrzębska 3a98d1edb1 add chat context 2024-09-21 18:06:26 +02:00
Aneta Jastrzębska 22d32aca3a save chat threads to workspace history 2024-09-21 18:05:25 +02:00
Aneta Jastrzębska fe5e02c3a9 unify toolbar button styles 2024-09-21 16:05:06 +02:00
Aneta Jastrzębska 3d58627f30 style included code tag + toolbar 2024-09-21 15:39:06 +02:00
Aneta Jastrzębska 20fd8f318b more vsc css vars 2024-09-21 15:36:48 +02:00
Aneta Jastrzębska 98ee94018e add className util 2024-09-21 15:36:25 +02:00
Aneta Jastrzębska ac9bc80e20 style included files list 2024-09-21 15:01:49 +02:00
Aneta Jastrzębska 8203b42169 hide entire toolbar in user messages 2024-09-21 14:14:31 +02:00
w1gs bc027b2fca resolve conflicts 2024-09-21 07:46:19 -04:00
Darion afa24231e6 Merge branch 'main' into main 2024-09-21 07:33:45 -04:00
w1gs da5bb4b68e updated styles 2024-09-21 07:27:19 -04:00
Dave Hogan f2d741d01f Merge branch 'voideditor:main' into main 2024-09-21 12:17:21 +01:00
Aneta Jastrzębska e893dc0327 add default style for small button 2024-09-21 13:12:22 +02:00
Aneta Jastrzębska e05a399177 add copy button 2024-09-21 13:03:31 +02:00
Aneta Jastrzębska 4f285ce364 rearrange markdown components 2024-09-21 13:03:00 +02:00
w1gs 2be5a2d13a Extension disabled when no keys set 2024-09-21 06:50:17 -04:00
w1gs 5e5fe80901 allowed urls updating when configuration changes 2024-09-21 04:56:41 -04:00
Jelf 44bc8bfdbf chore: update 2024-09-21 16:09:28 +08:00
Jelf 4f9b5ac873 chore: rm 2024-09-21 15:47:48 +08:00
Jelf 1beb011bfa refactor: configuration 2024-09-21 15:45:36 +08:00
Jelf bf9029f9bc chore: remove console.log 2024-09-21 14:56:15 +08:00
Jelf d7a3c44e31 chore: eslint 2024-09-21 14:54:27 +08:00
Jelf 7b31f2841a chore: improve settings (#1)
* refactor: refine `anthropic` and `openai` settings

* refactor: improve configuration

* chore: move to headers config

* fix: ollama baseURL, add description

* chore: change azure deploymentId

* chore: eslint

* chore: update allowed_urls
2024-09-21 14:16:27 +08:00
w1gs e8cf6ee9e7 Replaced fetch with Ollama client 2024-09-21 00:36:11 -04:00
w1gs 2b3f828654 Fixed parsing error in package.json 2024-09-21 00:24:51 -04:00
Darion 7ee1840e77 Merge branch 'voideditor:main' into main 2024-09-21 00:17:36 -04:00
Dave Hogan 1843cd0878 Makes anthropic model and max_tokens configurable
Makes whichApi an enum for less error prone configuration
2024-09-21 01:57:34 +01:00
Jelf 78e10a5593 feat: update ollama config 2024-09-21 02:15:19 +08:00
Darion 7bcefd4536 Update extensions/void/package.json
Co-authored-by: Bruce MacDonald <brucewmacdonald@gmail.com>
2024-09-20 13:56:21 -04:00
Jelf 6d5f885306 feat: ai sdk 2024-09-20 22:18:41 +08:00
Darion 99adb23149 Merge branch 'voideditor:main' into main 2024-09-19 13:09:43 -04:00
Darion 043a38bd36 Merge branch 'voideditor:main' into main 2024-09-18 18:55:54 -04:00
Darion 0269411c16 Merge branch 'voideditor:main' into main 2024-09-18 13:39:34 -04:00
w1gs f4ceb53738 Added Ollama integration 2024-09-18 02:02:43 -04:00
27 changed files with 1822 additions and 1251 deletions
+18 -5
View File
@@ -4,7 +4,7 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a
There are two main ways to contribute:
- Suggest New Features ([discord](https://discord.gg/4GAxHVAD))
- Suggest New Features ([discord](https://discord.gg/RSNjgaugJs))
- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3))
We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).
@@ -50,14 +50,14 @@ Now that you're set up, feel free to check out our [Issues](https://github.com/v
Beyond the extension, we very occasionally edit the IDE when we need to access more functionality. If you want to work on the full IDE, please follow the steps below, or see VS Code's full [how to contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
Before starting, make sure you've built the extension (by running `cd .\extensions\void\` and `npm run build`).
Before starting, make sure you've built the extension (by running `cd .\extensions\void\` and `npm run build`). Also make sure you have Python on your system.
Make sure you're on the correct NodeJS version as per `.nvmrc`.
1. Install all dependencies.
```
yarn
npm install
```
2. In VS Code, press <kbd>Ctrl+Shift+B</kbd> to start the build process - this can take some time. If you're not using VS Code, run `npm run watch` instead.
@@ -66,7 +66,7 @@ yarn
This should open up the built IDE after loading for some time. To see new changes without restarting the build, use <kbd>Ctrl+Shift+P</kbd> and run "Reload Window".
To bundle the IDE, run `npm run gulp vscode-win32-x64`. Here are the full options: vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin-arm64 | linux-ia32 | linux-x64 | linux-arm}(-min)
To bundle the IDE, run `npm run gulp vscode-darwin-arm64`. Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin-arm64 | linux-ia32 | linux-x64 | linux-arm}(-min)`
If you're on Windows, we recommend running the project inside a dev container. VSCode should prompt you to do this automatically.
@@ -124,8 +124,21 @@ Please don't make big refactors without speaking with us first. We'd like to kee
# Submitting a Pull Request
When you've made changes and want to submit them, please submit a pull request.
Please submit a pull request once you've made a change. Here are a few guidelines:
- A PR should be about one *single* feature change. The fewer items you change, the more likely the PR is to be accepted.
- Your PR should contain a description that first explains at a high level what you did, and then describes the exact changes you made (and to which files). Please don't use vague statements like "refactored code" or "improved types" (instead, describe what code you refactored, or what types you changed).
- Your title should clearly describe the change you made.
- Add tags to help us stay organized!
- Please don't open a new Issue for your PR. Just submit the PR.
- Avoid refactoring and making feature changes in the same PR.
- Write good code. For example, a common mistake when people edit Void's config is to hard-code a default value like `'claude-3.5'` in 2+ separate places. Please follow best practices or describe your thought process if you had to compromise.
# Relevant files
+1 -1
View File
@@ -8,7 +8,7 @@
"**/.DS_Store": true,
"**/Thumbs.db": true,
"out": false,
"**/node_modules": false
"**/node_modules": true
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
+10
View File
@@ -1 +1,11 @@
Please see the `CONTRIBUTING.md` for information on how to contribute :)!
Here's an overview on how the extension works:
- The extension mounts in `extension.ts`.
- The Sidebar's HTML (everything in `sidebar/`) is built in React, and it's rendered by mounting a `<script>` tag - see `SidebarWebviewProvider.ts`.
- Communication between the sidebar script and the extension takes place via API. You can search for "postMessage" to see where API calls happen.
+23 -422
View File
@@ -9,30 +9,32 @@
"version": "0.0.1",
"dependencies": {
"@anthropic-ai/sdk": "^0.27.1",
"diff-match-patch": "^1.0.5",
"diff": "^7.0.0",
"ollama": "^0.5.9",
"openai": "^4.57.0"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/diff": "^5.2.2",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.12",
"@types/mocha": "^10.0.8",
"@types/node": "^22.5.1",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
"@types/vscode": "1.92.0",
"@types/vscode": "1.89.0",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "2.4.1",
"autoprefixer": "^10.4.20",
"diff": "^6.0.0-beta",
"esbuild": "^0.23.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.1",
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0",
"marked": "^14.1.0",
"ollama": "^0.5.8",
"postcss": "^8.4.41",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -204,397 +206,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
@@ -1001,6 +612,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/diff-match-patch": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
"dev": true
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1152,9 +769,9 @@
"license": "MIT"
},
"node_modules/@types/vscode": {
"version": "1.92.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz",
"integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==",
"version": "1.89.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz",
"integrity": "sha512-TMfGKLSVxfGfoO8JfIE/neZqv7QLwS4nwPwL/NwMvxtAY2230H2I4Z5xx6836pmJvMAzqooRQ4pmLm7RUicP3A==",
"dev": true,
"license": "MIT"
},
@@ -2537,15 +2154,19 @@
"license": "Apache-2.0"
},
"node_modules/diff": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-6.0.0.tgz",
"integrity": "sha512-NbGtgPSw7il+jeajji1H6iKjCk3r/ANQKw3FFUhGV50+MH5MKIMeUmi53piTr7jlkWcq9eS858qbkRzkehwe+w==",
"dev": true,
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
},
"node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -3457,21 +3078,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4637,8 +4243,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
@@ -4810,7 +4415,6 @@
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -5973,7 +5577,6 @@
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz",
"integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-fetch": "^3.6.20"
@@ -6678,7 +6281,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -8182,7 +7784,6 @@
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"dev": true,
"license": "MIT"
},
"node_modules/whatwg-url": {
+260 -19
View File
@@ -1,5 +1,6 @@
{
"name": "void",
"publisher": "void",
"displayName": "Void",
"description": "",
"version": "0.0.1",
@@ -13,37 +14,242 @@
"main": "./out/extension.js",
"contributes": {
"configuration": {
"title": "API Keys",
"title": "Void",
"properties": {
"void.whichApi": {
"type": "string",
"default": "anthropic",
"description": "Choose a model to use (anthropic | openai | greptile | ollama)"
"description": "Choose an API provider.",
"enum": [
"openAI",
"openAICompatible",
"anthropic",
"azure",
"ollama",
"greptile"
]
},
"void.anthropicApiKey": {
"void.anthropic.apiKey": {
"type": "string",
"default": "",
"description": "Anthropic API Key"
"description": "Anthropic API key."
},
"void.openAIApiKey": {
"void.anthropic.model": {
"type": "string",
"default": "claude-3-5-sonnet-20240620",
"description": "Anthropic model to use.",
"enum": [
"claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307"
]
},
"void.anthropic.maxTokens": {
"type": "string",
"default": "8192",
"description": "Anthropic max number of tokens to output.",
"enum": [
"1024",
"2048",
"4096",
"8192"
]
},
"void.openAI.apiKey": {
"type": "string",
"default": "",
"description": "OpenAI API Key"
"description": "OpenAI API key."
},
"void.greptileApiKey": {
"void.openAI.model": {
"type": "string",
"default": "gpt-4o",
"description": "OpenAI model to use.",
"enum": [
"o1-preview",
"o1-mini",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"gpt-4o-mini",
"gpt-4o-mini-2024-07-18",
"gpt-4-turbo",
"gpt-4-turbo-2024-04-09",
"gpt-4-turbo-preview",
"gpt-4-0125-preview",
"gpt-4-1106-preview",
"gpt-4",
"gpt-4-0613",
"gpt-3.5-turbo-0125",
"gpt-3.5-turbo",
"gpt-3.5-turbo-1106"
]
},
"void.greptile.apiKey": {
"type": "string",
"default": "",
"description": "Greptile API Key"
"description": "Greptile API key."
},
"void.githubPAT": {
"void.greptile.githubPAT": {
"type": "string",
"default": "",
"description": "Greptile - Github PAT (gives Greptile access to your repo)"
"description": "Github PAT given to Greptile to access your repository."
},
"void.ollamaSettings": {
"void.greptile.remote": {
"type": "string",
"description": "remote provider",
"enum": [
"github",
"gitlab"
]
},
"void.greptile.repository": {
"type": "string",
"description": "Repository identifier in \"owner/repository\" format."
},
"void.greptile.branch": {
"type": "string",
"default": "main",
"description": "Name of the git branch."
},
"void.azure.apiKey": {
"type": "string",
"description": "Azure API key."
},
"void.azure.deploymentId": {
"type": "string",
"description": "Azure API deployment ID."
},
"void.azure.resourceName": {
"type": "string",
"description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`"
},
"void.azure.providerSettings": {
"type": "object",
"properties": {
"baseURL": {
"type": "string",
"default": "https://${resourceName}.openai.azure.com/openai/deployments",
"description": "Azure API base URL."
},
"headers": {
"type": "object",
"description": "Custom headers to include in the requests."
}
}
},
"void.ollama.endpoint": {
"type": "string",
"default": "http://127.0.0.1:11434",
"description": "The Ollama endpoint. Start Ollama by running `OLLAMA_ORIGINS=\"vscode-webview://*\" ollama serve`"
},
"void.ollama.model": {
"type": "string",
"default": "llama3.1",
"description": "Ollama model to use.",
"enum": [
"codegemma",
"codegemma:2b",
"codegemma:7b",
"codellama",
"codellama:7b",
"codellama:13b",
"codellama:34b",
"codellama:70b",
"codellama:code",
"codellama:python",
"command-r",
"command-r:35b",
"command-r-plus",
"command-r-plus:104b",
"deepseek-coder-v2",
"deepseek-coder-v2:16b",
"deepseek-coder-v2:236b",
"falcon2",
"falcon2:11b",
"firefunction-v2",
"firefunction-v2:70b",
"gemma",
"gemma:2b",
"gemma:7b",
"gemma2",
"gemma2:2b",
"gemma2:9b",
"gemma2:27b",
"llama2",
"llama2:7b",
"llama2:13b",
"llama2:70b",
"llama3",
"llama3:8b",
"llama3:70b",
"llama3-chatqa",
"llama3-chatqa:8b",
"llama3-chatqa:70b",
"llama3-gradient",
"llama3-gradient:8b",
"llama3-gradient:70b",
"llama3.1",
"llama3.1:8b",
"llama3.1:70b",
"llama3.1:405b",
"llava",
"llava:7b",
"llava:13b",
"llava:34b",
"llava-llama3",
"llava-llama3:8b",
"llava-phi3",
"llava-phi3:3.8b",
"mistral",
"mistral:7b",
"mistral-large",
"mistral-large:123b",
"mistral-nemo",
"mistral-nemo:12b",
"mixtral",
"mixtral:8x7b",
"mixtral:8x22b",
"moondream",
"moondream:1.8b",
"openhermes",
"openhermes:v2.5",
"phi3",
"phi3:3.8b",
"phi3:14b",
"phi3.5",
"phi3.5:3.8b",
"qwen",
"qwen:7b",
"qwen:14b",
"qwen:32b",
"qwen:72b",
"qwen:110b",
"qwen2",
"qwen2:0.5b",
"qwen2:1.5b",
"qwen2:7b",
"qwen2:72b",
"smollm",
"smollm:135m",
"smollm:360m",
"smollm:1.7b"
]
},
"void.openAICompatible.endpoint": {
"type": "string",
"default": "http://127.0.0.1:11434/v1",
"description": "The endpoint."
},
"void.openAICompatible.model": {
"type": "string",
"default": "gpt-4o",
"description": "The name of the model to use."
},
"void.openAICompatible.apiKey": {
"type": "string",
"default": "",
"description": "Ollama settings (coming soon...)"
"description": "Your API key."
}
}
},
@@ -57,12 +263,26 @@
"title": "Show Selection Lens"
},
{
"command": "void.approveDiff",
"command": "void.acceptDiff",
"title": "Approve Diff"
},
{
"command": "void.discardDiff",
"command": "void.rejectDiff",
"title": "Discard Diff"
},
{
"command": "void.startNewThread",
"title": "Start a new chat",
"icon": "$(add)"
},
{
"command": "void.toggleThreadSelector",
"title": "View past chats",
"icon": "$(history)"
}, {
"command": "void.openSettings",
"title": "Void settings",
"icon": "$(settings-gear)"
}
],
"viewsContainers": {
@@ -94,7 +314,26 @@
"key": "ctrl+k",
"mac": "cmd+k"
}
]
],
"menus": {
"view/title": [
{
"command": "void.startNewThread",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{
"command": "void.toggleThreadSelector",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{
"command": "void.openSettings",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
@@ -106,25 +345,24 @@
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/diff": "^5.2.2",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.12",
"@types/mocha": "^10.0.8",
"@types/node": "^22.5.1",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
"@types/vscode": "1.92.0",
"@types/vscode": "1.89.0",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "2.4.1",
"autoprefixer": "^10.4.20",
"diff": "^6.0.0-beta",
"esbuild": "^0.23.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.1",
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0",
"marked": "^14.1.0",
"ollama": "^0.5.8",
"postcss": "^8.4.41",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -136,6 +374,9 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.27.1",
"openai": "^4.57.0"
"diff-match-patch": "^1.0.5",
"ollama": "^0.5.9",
"openai": "^4.57.0",
"diff": "^7.0.0"
}
}
@@ -1,186 +0,0 @@
import * as vscode from 'vscode';
import { SuggestedEdit } from './getDiffedLines';
// each diff on the user's screen right now
type DiffType = {
diffid: number,
lenses: vscode.CodeLens[],
greenRange: vscode.Range,
originalCode: string, // If a revert happens, we replace the greenRange with this content.
}
// TODO in theory this should be disposed
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
export class ApprovalCodeLensProvider implements vscode.CodeLensProvider {
private _diffsOfDocument: { [docUriStr: string]: DiffType[] } = {};
private _computedLensesOfDocument: { [docUriStr: string]: vscode.CodeLens[] } = {} // computed from diffsOfDocument[docUriStr].lenses
private _diffidPool = 0
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
private _weAreEditing: boolean = false
// used internally by vscode
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// used internally by vscode
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const docUriStr = document.uri.toString()
return this._computedLensesOfDocument[docUriStr]
}
// declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor() {
// this acts as a useEffect. Every time text changes, clear the diffs in this editor
vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor
if (!editor)
return
if (this._weAreEditing)
return
const docUri = editor.document.uri
const docUriStr = docUri.toString()
this._diffsOfDocument[docUriStr].splice(0) // clear diffs
editor.setDecorations(greenDecoration, []) // clear decorations
this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute
this._onDidChangeCodeLenses.fire() // refresh
})
}
// used by us only
private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => {
editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh highlighting
this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute _computedLensesOfDocument (can optimize this later)
this._onDidChangeCodeLenses.fire() // fire event for vscode to refresh lenses
}
// used by us only
public async addNewApprovals(editor: vscode.TextEditor, suggestedEdits: SuggestedEdit[]) {
const docUri = editor.document.uri
const docUriStr = docUri.toString()
if (!this._diffsOfDocument[docUriStr])
this._diffsOfDocument[docUriStr] = []
if (!this._computedLensesOfDocument[docUriStr])
this._computedLensesOfDocument[docUriStr] = []
// 1. convert suggested edits (which are described using line numbers) into actual edits (described using vscode.Range, vscode.Uri)
// must do this before adding codelenses or highlighting so that codelens and highlights will apply to the fresh code and not the old code
// apply changes in reverse order so additions don't push down the line numbers of the next edit
let workspaceEdit = new vscode.WorkspaceEdit();
for (let i = suggestedEdits.length - 1; i > -1; i -= 1) {
let suggestedEdit = suggestedEdits[i]
let greenRange: vscode.Range
// INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1})
if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) {
const originalPosition = new vscode.Position(suggestedEdit.originalStartLine, 0)
workspaceEdit.insert(docUri, originalPosition, suggestedEdit.newContent + '\n') // add back in the line we deleted when we made the startline->endline range go negative
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine + 1, 0)
}
// DELETIONS
else if (suggestedEdit.startLine > suggestedEdit.endLine) {
const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0)
workspaceEdit.delete(docUri, deleteRange)
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.startLine, 0)
suggestedEdit.originalContent += '\n' // add back in the line we deleted when we made the startline->endline range go negative
}
// REPLACEMENTS
else {
const originalRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine, Number.MAX_SAFE_INTEGER)
workspaceEdit.replace(docUri, originalRange, suggestedEdit.newContent)
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine, Number.MAX_SAFE_INTEGER)
}
this._diffsOfDocument[docUriStr].push({
diffid: this._diffidPool,
greenRange: greenRange,
originalCode: suggestedEdit.originalContent,
lenses: [
new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.approveDiff', arguments: [{ diffid: this._diffidPool }] }),
new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.discardDiff', arguments: [{ diffid: this._diffidPool }] })
]
});
this._diffidPool += 1
}
this._weAreEditing = true
await vscode.workspace.applyEdit(workspaceEdit)
await vscode.workspace.save(docUri)
this._weAreEditing = false
// refresh
this.refreshLenses(editor, docUriStr)
console.log('diffs after added:', this._diffsOfDocument[docUriStr])
}
// called on void.approveDiff
public async approveDiff({ diffid }: { diffid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
const docUri = editor.document.uri
const docUriStr = docUri.toString()
// get index of this diff in diffsOfDocument
const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (index === -1) {
console.error('Error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr])
return
}
// remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history)
this._diffsOfDocument[docUriStr].splice(index, 1)
// refresh
this.refreshLenses(editor, docUriStr)
}
// called on void.discardDiff
public async discardDiff({ diffid }: { diffid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
const docUri = editor.document.uri
const docUriStr = docUri.toString()
// get index of this diff in diffsOfDocument
const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (index === -1) {
console.error('Void error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr])
return
}
const { greenRange: range, lenses, originalCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index
// remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history)
this._diffsOfDocument[docUriStr].splice(index, 1)
// clear the decoration in this diffs range
editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange))
// REVERT THE CHANGE (this is the only part that's different from approveDiff)
let workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(docUri, range, originalCode);
this._weAreEditing = true
await vscode.workspace.applyEdit(workspaceEdit)
await vscode.workspace.save(docUri)
this._weAreEditing = false
// refresh
this.refreshLenses(editor, docUriStr)
}
}
@@ -1,31 +0,0 @@
import * as vscode from 'vscode';
export class CtrlKCodeLensProvider implements vscode.CodeLensProvider {
private codelensesOfDocument: { [documentUri: string]: vscode.CodeLens[] } = {};
// only called by vscode's internals
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const docUri = document.uri.toString()
return this.codelensesOfDocument[docUri];
}
// only called by us
public addNewCodeLens(document: vscode.TextDocument, selection: vscode.Selection) {
const docUri = document.uri.toString()
if (!this.codelensesOfDocument[docUri])
this.codelensesOfDocument[docUri] = []
// if any other codelens intersects with the selection, don't do it (and have the user now focus that codelens)
for (let lens of this.codelensesOfDocument[docUri]) {
if (lens.range.intersection(selection))
return
}
this.codelensesOfDocument[docUri] = [
...this.codelensesOfDocument[docUri],
new vscode.CodeLens(new vscode.Range(selection.start.line, 0, selection.end.line, Infinity), { title: '', command: '' })];
}
}
@@ -0,0 +1,315 @@
import * as vscode from 'vscode';
import { findDiffs } from './findDiffs';
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './shared_types';
// TODO in theory this should be disposed
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
// responsible for displaying diffs and showing accept/reject buttons
export class DisplayChangesProvider implements vscode.CodeLensProvider {
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
private _diffareaidPool = 0
private _diffidPool = 0
private _weAreEditing: boolean = false
// used internally by vscode
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// used internally by vscode
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const docUriStr = document.uri.toString()
return this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses)
}
// declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor() {
console.log('Creating DisplayChangesProvider')
// this acts as a useEffect. Every time text changes, clear the diffs in this editor
vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor
if (!editor)
return
if (this._weAreEditing)
return
const docUri = editor.document.uri
const docUriStr = docUri.toString()
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// loop through each change
for (const change of e.contentChanges) {
// here, `change.range` is the range of the original file that gets replaced with `change.text`
// compute net number of newlines lines that were added/removed
const numNewLines = (change.text.match(/\n/g) || []).length
const numLineDeletions = change.range.end.line - change.range.start.line
const deltaNewlines = numNewLines - numLineDeletions
// compute overlap with each diffArea and shrink/elongate the diffArea accordingly
for (const diffArea of diffAreas) {
// if the change is fully within the diffArea, elongate it by the delta amount of newlines
if (change.range.start.line >= diffArea.startLine && change.range.end.line <= diffArea.endLine) {
diffArea.endLine += deltaNewlines
}
// check if the `diffArea` was fully deleted and remove it if so
if (diffArea.startLine > diffArea.endLine) {
//remove it
const index = diffAreas.findIndex(da => da === diffArea)
diffAreas.splice(index, 1)
}
// TODO handle other cases where eg. the change overlaps many diffAreas
}
// if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
for (const diffArea of diffAreas) {
if (diffArea.startLine > change.range.end.line) {
diffArea.startLine += deltaNewlines
diffArea.endLine += deltaNewlines
}
}
// TODO merge any diffAreas if they overlap with each other as a result from the shift
}
// refresh the diffAreas
this.refreshDiffAreas(docUri)
})
}
// used by us only
public addDiffArea(uri: vscode.Uri, diffArea: BaseDiffArea) {
const uriStr = uri.toString()
// make sure array is defined
if (!this._diffAreasOfDocument[uriStr])
this._diffAreasOfDocument[uriStr] = []
// remove all diffAreas that the new `diffArea` is overlapping with
this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
const noOverlap = da.startLine > diffArea.endLine || da.endLine < diffArea.startLine
if (!noOverlap) return false
return true
})
// add `diffArea` to storage
this._diffAreasOfDocument[uriStr].push({
...diffArea,
diffareaid: this._diffareaidPool
})
this._diffareaidPool += 1
}
// used by us only
public refreshDiffAreas(docUri: vscode.Uri) {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
console.log('Error: No active editor!')
return;
}
const docUriStr = docUri.toString()
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// reset all diffs (we update them below)
this._diffsOfDocument[docUriStr] = []
// for each diffArea
for (const diffArea of diffAreas) {
// get code inside of diffArea
const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n')
// compute the diffs
const diffs = findDiffs(diffArea.originalCode, currentCode)
// print diffs
console.log('!CODEBefore:', JSON.stringify(diffArea.originalCode))
console.log('!CODEAfter:', JSON.stringify(currentCode))
// add the diffs to `this._diffsOfDocument[docUriStr]`
this.addDiffs(editor.document.uri, diffs, diffArea)
for (const diff of this._diffsOfDocument[docUriStr]) {
console.log('------------')
console.log('deletedCode:', JSON.stringify(diff.deletedCode))
console.log('insertedCode:', JSON.stringify(diff.insertedCode))
console.log('deletedRange:', diff.deletedRange.start.line, diff.deletedRange.end.line,)
console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,)
}
}
// update green highlighting
editor.setDecorations(
greenDecoration,
(this._diffsOfDocument[docUriStr]
.filter(diff => diff.insertedRange !== undefined)
.map(diff => diff.insertedRange)
)
);
// TODO update red highlighting
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
// update code lenses
this._onDidChangeCodeLenses.fire()
}
// used by us only
public addDiffs(docUri: vscode.Uri, diffs: BaseDiff[], diffArea: DiffArea) {
const docUriStr = docUri.toString()
// if no diffs, set diffs to []
if (!this._diffsOfDocument[docUriStr])
this._diffsOfDocument[docUriStr] = []
// add each diff and its codelens to the document
for (let i = diffs.length - 1; i > -1; i -= 1) {
let suggestedDiff = diffs[i]
this._diffsOfDocument[docUriStr].push({
...suggestedDiff,
diffid: this._diffidPool,
// originalCode: suggestedDiff.deletedText,
lenses: [
new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
]
});
this._diffidPool += 1
}
}
// called on void.acceptDiff
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
// get document uri
const docUri = editor.document.uri
const docUriStr = docUri.toString()
// get relevant diff
// TODO speed up with hashmap
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) {
console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
// get relevant diffArea
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) {
console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
const diff = this._diffsOfDocument[docUriStr][diffIdx]
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// replace `originalCode[diff.deletedRange]` with diff.insertedCode
// TODO add a history event to undo this change
const originalLines = diffArea.originalCode.split('\n');
const relativeStart = diff.deletedRange.start.line - diffArea.originalStartLine
const relativeEnd = diff.deletedRange.end.line - diffArea.originalStartLine
diffArea.originalCode = [
...originalLines.slice(0, relativeStart), // lines before the deleted range
...diff.insertedCode.split('\n'), // inserted lines
...originalLines.slice(relativeEnd + 1) // lines after the deleted range
].join('\n')
// if the diffArea has no changes, remove it
const currentDiffAreaCode = editor.document.getText()
.replace(/\r\n/g, '\n')
.split('\n')
.slice(diffArea.startLine, diffArea.endLine + 1)
.join('\n')
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
// refresh the diff area
this.refreshDiffAreas(docUri)
}
// called on void.rejectDiff
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
// get document uri
const docUri = editor.document.uri
const docUriStr = docUri.toString()
// get relevant diff
// TODO speed up with hashmap
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) {
console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
// get relevant diffArea
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) {
console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
const diff = this._diffsOfDocument[docUriStr][diffIdx]
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// replace `editorCode[diff.insertedRange]` with diff.deletedCode
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(docUri, diff.insertedRange, diff.deletedCode)
this._weAreEditing = true
await vscode.workspace.applyEdit(workspaceEdit)
this._weAreEditing = false
// if the diffArea has no changes, remove it
const currentDiffAreaCode = editor.document.getText()
.replace(/\r\n/g, '\n')
.split('\n')
.slice(diffArea.startLine, diffArea.endLine + 1)
.join('\n')
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
// refresh the diff area
this.refreshDiffAreas(docUri)
}
}
+61 -34
View File
@@ -14,21 +14,74 @@ function getNonce() {
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
public static readonly viewId = 'void.viewnumberone';
public webview: Promise<vscode.Webview> // used to send messages to the webview
private readonly _extensionUri: vscode.Uri
public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
private _res: (c: vscode.Webview) => void // used to resolve the webview
private readonly _extensionUri: vscode.Uri
private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration
private _webviewDeps: string[] = [];
constructor(context: vscode.ExtensionContext) {
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later, not sure for what though... was included in webviewProvider code
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
this._extensionUri = context.extensionUri
let temp_res: typeof this._res | undefined = undefined
this.webview = new Promise((res, rej) => { temp_res = res })
if (!temp_res) throw new Error("sidebar provider: resolver was undefined")
this._res = temp_res
// if it affects one of the config items webview depends on, update the webview
// TODO should be able to move this entirely to React - make updateWebviewHTML mount once, and then send updates via postMessage from then on
vscode.workspace.onDidChangeConfiguration(event => {
if (this._webviewDeps.map(dep => event.affectsConfiguration(dep)).some(v => !!v)) {
if (this._webviewView) {
this.updateWebviewHTML(this._webviewView.webview);
}
}
});
}
// this is updated
private updateWebviewHTML(webview: vscode.Webview) {
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com'];
this._webviewDeps = []
const ollamaEndpoint: string | undefined = vscode.workspace.getConfiguration('void.ollama').get('endpoint');
this._webviewDeps.push('void.ollama.endpoint');
if (ollamaEndpoint)
allowed_urls.push(ollamaEndpoint);
const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint');
this._webviewDeps.push('void.openAICompatible.endpoint');
if (openAICompatibleEndpoint)
allowed_urls.push(openAICompatibleEndpoint);
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri));
const nonce = getNonce();
const webviewHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom View</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ${allowed_urls.join(' ')}; img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
webview.html = webviewHTML;
}
// called internally by vscode
resolveWebviewView(
webviewView: vscode.WebviewView,
@@ -36,43 +89,17 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
token: vscode.CancellationToken,
) {
const webview = webviewView.webview
const webview = webviewView.webview;
webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
// This allows us to use React in vscode
// when you run `npm run build`, we take the React code in the `sidebar` folder
// and compile it into `dist/sidebar/index.js` and `dist/sidebar/styles.css`
// we render that code here
const rootPath = this._extensionUri;
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/index.js'));
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/styles.css'));
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath));
const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com']
webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom View</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ${allowed_urls.join(' ')}; img-src vscode-resource: https:; script-src 'nonce-${nonce}';style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
this.updateWebviewHTML(webview);
// resolve webview and _webviewView
this._res(webview);
this._webviewView = webviewView;
}
}
+102 -87
View File
@@ -1,14 +1,18 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
// import ollama from 'ollama'
// always compare these against package.json to make sure every setting in this type can actually be provided by the user
export type ApiConfig = {
anthropic: {
apikey: string,
model: string,
maxTokens: string
},
openai: {
apikey: string
openAI: {
apikey: string,
model: string,
},
greptile: {
apikey: string,
@@ -20,8 +24,14 @@ export type ApiConfig = {
}
},
ollama: {
// TODO
endpoint: string,
model: string
},
openAICompatible: {
endpoint: string,
model: string,
apikey: string
}
whichApi: string
}
@@ -60,12 +70,11 @@ type SendLLMMessageFnTypeExternal = (params: {
// Claude
const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
const anthropic = new Anthropic({ apiKey: apiConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
const stream = anthropic.messages.stream({
model: "claude-3-5-sonnet-20240620",
max_tokens: 1024,
model: apiConfig.anthropic.model,
max_tokens: parseInt(apiConfig.anthropic.maxTokens),
messages: messages,
});
@@ -99,31 +108,44 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
// OpenAI
// OpenAI and OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let did_abort = false
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort: () => void = () => { did_abort = true }
let abort: () => void = () => {
didAbort = true;
};
const openai = new OpenAI({ apiKey: apiConfig.openai.apikey, dangerouslyAllowBrowser: true });
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
openai.chat.completions.create({
model: 'gpt-4o-2024-08-06',
messages: messages,
stream: true,
})
if (apiConfig.whichApi === 'openAI') {
openai = new OpenAI({ baseURL: apiConfig.openAICompatible.endpoint, apiKey: apiConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
options = { model: apiConfig.openAI.model, messages: messages, stream: true, }
}
else if (apiConfig.whichApi === 'openAICompatible') {
openai = new OpenAI({ apiKey: apiConfig.openAI.apikey, dangerouslyAllowBrowser: true });
options = { model: apiConfig.openAICompatible.model, messages: messages, stream: true, }
}
else {
console.error(`sendOpenAIMsg: invalid whichApi: ${apiConfig.whichApi}`)
throw new Error(`apiConfig.whichAPI was invalid: ${apiConfig.whichApi}`)
}
openai.chat.completions
.create(options)
.then(async response => {
abort = () => {
// response.controller.abort() // this isn't needed now, to keep consistency with claude will leave it commented
did_abort = true;
// response.controller.abort()
didAbort = true;
}
// when receive text
try {
for await (const chunk of response) {
if (did_abort) return;
if (didAbort) return;
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText(newText, fullText);
@@ -135,8 +157,49 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
console.error('Error in OpenAI stream:', error);
onFinalMessage(fullText);
}
// when we get the final message on this stream
onFinalMessage(fullText)
})
return { abort };
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let didAbort = false
let fullText = ""
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort = () => {
didAbort = true;
};
const ollama = new Ollama({ host: apiConfig.ollama.endpoint })
ollama.chat({
model: apiConfig.ollama.model,
messages: messages,
stream: true,
})
.then(async stream => {
abort = () => {
// ollama.abort()
didAbort = true
}
// iterate through the stream
try {
for await (const chunk of stream) {
if (didAbort) return;
const newText = chunk.message.content;
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
}
// when error/fail
catch (error) {
console.error('Error:', error);
onFinalMessage(fullText);
}
})
return { abort };
};
@@ -149,11 +212,11 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let did_abort = false
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort: () => void = () => { did_abort = true }
let abort: () => void = () => { didAbort = true }
fetch('https://api.greptile.com/v2/query', {
@@ -177,7 +240,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
})
// TODO make this actually stream, right now it just sends one message at the end
.then(async responseArr => {
if (did_abort)
if (didAbort)
return
for (let response of responseArr) {
@@ -212,74 +275,26 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
return { abort }
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
if (!apiConfig) return { abort: () => { } }
const whichApi = apiConfig.whichApi
if (whichApi === 'anthropic') {
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig })
switch (apiConfig.whichApi) {
case 'anthropic':
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig });
case 'openAI':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig });
case 'greptile':
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig });
case 'ollama':
return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig });
default:
console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`);
return { abort: () => { } }
//return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO
}
else if (whichApi === 'openai') {
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig })
}
else if (whichApi === 'greptile') {
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig })
}
else if (whichApi === 'ollama') {
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO
}
else {
console.error(`Error: whichApi was ${whichApi}, which is not recognized!`)
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO
}
}
// Ollama
// const sendOllamaMsg: sendMsgFnType = ({ messages, onText, onFinalMessage }) => {
// let did_abort = false
// let fullText = ''
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// let abort: () => void = () => {
// did_abort = true
// }
// ollama.chat({ model: 'llama3.1', messages: messages, stream: true })
// .then(async response => {
// abort = () => {
// // response.abort() // this isn't needed now, to keep consistency with claude will leave it commented for now
// did_abort = true;
// }
// // when receive text
// try {
// for await (const part of response) {
// if (did_abort) return
// let newText = part.message.content
// fullText += newText
// onText(newText, fullText)
// }
// }
// // when error/fail
// catch (e) {
// onFinalMessage(fullText)
// return
// }
// // when we get the final message on this stream
// onFinalMessage(fullText)
// })
// return { abort };
// };
+76 -26
View File
@@ -1,23 +1,29 @@
import * as vscode from 'vscode';
import { WebviewMessage } from './shared_types';
import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider';
import { getDiffedLines } from './getDiffedLines';
import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider';
import { DisplayChangesProvider } from './DisplayChangesProvider';
import { BaseDiffArea, ChatThreads, WebviewMessage } from './shared_types';
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
import { ApiConfig } from './common/sendLLMMessage';
const readFileContentOfUri = async (uri: vscode.Uri) => {
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8').replace(/\r\n/g, '\n'); // must remove windows \r or every line will appear different because of it
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
.replace(/\r\n/g, '\n') // replace windows \r\n with \n
}
const getApiConfig = () => {
const apiConfig: ApiConfig = {
anthropic: { apikey: vscode.workspace.getConfiguration('void').get('anthropicApiKey') ?? '' },
openai: { apikey: vscode.workspace.getConfiguration('void').get('openAIApiKey') ?? '' },
anthropic: {
apikey: vscode.workspace.getConfiguration('void.anthropic').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void.anthropic').get('model') ?? '',
maxTokens: vscode.workspace.getConfiguration('void.anthropic').get('maxTokens') ?? '',
},
openAI: {
apikey: vscode.workspace.getConfiguration('void.openAI').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void.openAI').get('model') ?? '',
},
greptile: {
apikey: vscode.workspace.getConfiguration('void').get('greptileApiKey') ?? '',
githubPAT: vscode.workspace.getConfiguration('void').get('githubPAT') ?? '',
apikey: vscode.workspace.getConfiguration('void.greptile').get('apiKey') ?? '',
githubPAT: vscode.workspace.getConfiguration('void.greptile').get('githubPAT') ?? '',
repoinfo: {
remote: 'github',
repository: 'TODO',
@@ -25,13 +31,20 @@ const getApiConfig = () => {
}
},
ollama: {
// apikey: vscode.workspace.getConfiguration('void').get('ollamaSettings') ?? '',
endpoint: vscode.workspace.getConfiguration('void.ollama').get('endpoint') ?? '',
model: vscode.workspace.getConfiguration('void.ollama').get('model') ?? '',
},
openAICompatible: {
endpoint: vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint') ?? '',
apikey: vscode.workspace.getConfiguration('void.openAICompatible').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void.openAICompatible').get('model') ?? '',
},
whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? ''
}
return apiConfig
}
export function activate(context: vscode.ExtensionContext) {
// 1. Mount the chat sidebar
@@ -67,22 +80,33 @@ export function activate(context: vscode.ExtensionContext) {
);
// 3. Show an approve/reject codelens above each change
const approvalCodeLensProvider = new ApprovalCodeLensProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', approvalCodeLensProvider));
const displayChangesProvider = new DisplayChangesProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider));
// 4. Add approve/reject commands
context.subscriptions.push(vscode.commands.registerCommand('void.approveDiff', async (params) => {
approvalCodeLensProvider.approveDiff(params)
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
displayChangesProvider.acceptDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.discardDiff', async (params) => {
approvalCodeLensProvider.discardDiff(params)
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
displayChangesProvider.rejectDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.openSettings', async () => {
vscode.commands.executeCommand('workbench.action.openSettings', '@ext:void.void');
}));
// 5.
// 5. Receive messages from sidebar
webviewProvider.webview.then(
webview => {
// top navigation bar commands
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
webview.postMessage({ type: 'toggleThreadSelector' } satisfies WebviewMessage)
}))
// when config changes, send it to the sidebar
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('void')) {
@@ -105,27 +129,53 @@ export function activate(context: vscode.ExtensionContext) {
// send contents to webview
webview.postMessage({ type: 'files', files, } satisfies WebviewMessage)
} else if (m.type === 'applyCode') {
} else if (m.type === 'applyChanges') {
const editor = vscode.window.activeTextEditor
if (!editor) {
vscode.window.showInformationMessage('No active editor!')
return
}
const oldContents = await readFileContentOfUri(editor.document.uri)
const suggestedEdits = getDiffedLines(oldContents, m.code)
await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits)
// create an area to show diffs
const diffArea: BaseDiffArea = {
startLine: 0, // in ctrl+L the start and end lines are the full document
endLine: editor.document.lineCount,
originalStartLine: 0,
originalEndLine: editor.document.lineCount,
originalCode: await readFileContentOfUri(editor.document.uri),
}
displayChangesProvider.addDiffArea(editor.document.uri, diffArea)
// write new code `m.code` to the document
// TODO update like this:
// this._weAreEditing = true
// await vscode.workspace.applyEdit(workspaceEdit)
// await vscode.workspace.save(docUri)
// this._weAreEditing = false
await editor.edit(editBuilder => {
editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code);
});
// rediff the changes based on the diffAreas
displayChangesProvider.refreshDiffAreas(editor.document.uri)
}
else if (m.type === 'getApiConfig') {
const apiConfig = getApiConfig()
console.log('Api config:', apiConfig)
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
}
else if (m.type === 'getAllThreads') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
webview.postMessage({ type: 'allThreads', threads } satisfies WebviewMessage)
}
else if (m.type === 'persistThread') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
context.workspaceState.update('allThreads', updatedThreads)
}
else {
console.error('unrecognized command', m.type, m)
}
})
+277
View File
@@ -0,0 +1,277 @@
import * as vscode from 'vscode';
// import { diffLines, Change } from 'diff';
import { BaseDiff } from './shared_types';
import { diff_match_patch } from 'diff-match-patch';
const diffLines = (text1: string, text2: string) => {
var dmp = new diff_match_patch();
var a = dmp.diff_linesToChars_(text1, text2);
var lineText1 = a.chars1;
var lineText2 = a.chars2;
var lineArray = a.lineArray;
var diffs = dmp.diff_main(lineText1, lineText2, false);
dmp.diff_charsToLines_(diffs, lineArray);
// dmp.diff_cleanupSemantic(diffs);
return diffs;
}
// TODO use a better diff algorithm
export const findDiffs = (oldText: string, newText: string): BaseDiff[] => {
const diffs = diffLines(oldText, newText);
const blocks: BaseDiff[] = [];
let reprBlock: string[] = [];
let deletedBlock: string[] = [];
let insertedBlock: string[] = [];
let insertedLine = 0;
let deletedLine = 0;
let insertedStart = 0;
let deletedStart = 0;
diffs.forEach(([operation, text]) => {
const lines = text.split('\n');
switch (operation) {
// insertion
case 1:
if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
if (insertedBlock.length === 0) insertedStart = insertedLine;
insertedLine += lines.length - 1; // Update only the line count for new text
insertedBlock.push(text);
reprBlock.push(lines.map(line => `+ ${line}`).join('\n'));
break;
// deletion
case -1:
if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
if (deletedBlock.length === 0) deletedStart = deletedLine;
deletedLine += lines.length - 1; // Update only the line count for old text
deletedBlock.push(text);
reprBlock.push(lines.map(line => `- ${line}`).join('\n'));
break;
// no change
case 0:
// If we have a pending block, add it to the blocks array
if (insertedBlock.length > 0 || deletedBlock.length > 0) {
blocks.push({
code: reprBlock.join(''),
deletedCode: deletedBlock.join(''),
insertedCode: insertedBlock.join(''),
deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
});
}
// Reset the block variables
reprBlock = [];
deletedBlock = [];
insertedBlock = [];
// Update line counts for unchanged text
insertedLine += lines.length - 1;
deletedLine += lines.length - 1;
break;
}
});
// Add any remaining blocks after the loop ends
if (insertedBlock.length > 0 || deletedBlock.length > 0) {
blocks.push({
code: reprBlock.join(''),
deletedCode: deletedBlock.join(''),
insertedCode: insertedBlock.join(''),
deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
});
}
return blocks;
};
// export const findDiffs = (oldText: string, newText: string): DiffBlock[] => {
// const diffs = diffLines(oldText, newText);
// const blocks: DiffBlock[] = [];
// let reprBlock: string[] = [];
// let deletedBlock: string[] = [];
// let insertedBlock: string[] = [];
// let insertedEnd = 0;
// let deletedEnd = 0;
// let insertedStart = 0;
// let deletedStart = 0;
// diffs.forEach(part => {
// part.count = part.count ?? 0
// // if the part is an addition or deletion, add it to the current block
// if (part.added || part.removed) {
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
// if (part.added) {
// if (insertedBlock.length === 0) insertedStart = insertedEnd;
// insertedEnd += part.count
// insertedBlock.push(part.value);
// reprBlock.push(part.value.split('\n').map(line => `+ ${line}`).join('\n'));
// }
// if (part.removed) {
// if (deletedBlock.length === 0) deletedStart = deletedEnd;
// deletedEnd += part.count
// deletedBlock.push(part.value);
// reprBlock.push(part.value.split('\n').map(line => `- ${line}`).join('\n'));
// }
// }
// // if the part is unchanged, finalize the block and add it to the array
// else {
// // if the block is not null, add it to the array
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
// blocks.push({
// code: reprBlock.join('\n'),
// deletedCode: deletedBlock.join(''),
// insertedCode: insertedBlock.join(''),
// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER),
// });
// }
// // update block variables
// reprBlock = [];
// deletedBlock = [];
// insertedBlock = [];
// insertedEnd += part.count;
// deletedEnd += part.count;
// }
// })
// // finally, add the last block to the array
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
// blocks.push({
// code: reprBlock.join('\n'),
// deletedCode: deletedBlock.join(''),
// insertedCode: insertedBlock.join(''),
// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER),
// });
// }
// return blocks;
// }
// import { diffLines, Change } from 'diff';
// export type SuggestedEdit = {
// // start/end of current file
// startLine: number;
// endLine: number;
// // start/end of original file
// originalStartLine: number,
// originalEndLine: number,
// // original content (originalfile[originalStart...originalEnd])
// originalContent: string;
// newContent: string;
// }
// export function getDiffedLines(oldStr: string, newStr: string) {
// // an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
// const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
// console.debug('Line by line changes', lineByLineChanges)
// lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
// let oldFileLineNum: number = 0;
// let newFileLineNum: number = 0;
// let streakStartInNewFile: number | undefined = undefined
// let streakStartInOldFile: number | undefined = undefined
// let oldStrLines = oldStr.split('\n')
// let newStrLines = newStr.split('\n')
// const replacements: SuggestedEdit[] = []
// for (let line of lineByLineChanges) {
// // no change on this line
// if (!line.added && !line.removed) {
// // if we were on a streak, add it
// if (streakStartInNewFile !== undefined) {
// const startLine = streakStartInNewFile
// const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
// const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
// const originalStartLine = streakStartInOldFile!
// const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
// const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
// const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent }
// replacements.push(replacement)
// streakStartInNewFile = undefined
// streakStartInOldFile = undefined
// }
// oldFileLineNum += line.count ?? 0;
// newFileLineNum += line.count ?? 0;
// }
// // line was removed from old file
// else if (line.removed) {
// // if we weren't on a streak, start one on this current line num
// if (streakStartInNewFile === undefined) {
// streakStartInNewFile = newFileLineNum
// streakStartInOldFile = oldFileLineNum
// }
// oldFileLineNum += line.count ?? 0 // we processed the line so add 1
// }
// // line was added to new file
// else if (line.added) {
// // if we weren't on a streak, start one on this current line num
// if (streakStartInNewFile === undefined) {
// streakStartInNewFile = newFileLineNum
// streakStartInOldFile = oldFileLineNum
// }
// newFileLineNum += line.count ?? 0; // we processed the line so add 1
// }
// } // end for
// console.debug('Replacements', replacements)
// return replacements
// }
-91
View File
@@ -1,91 +0,0 @@
import { diffLines, Change } from 'diff';
export type SuggestedEdit = {
// start/end of current file
startLine: number;
endLine: number;
// start/end of original file
originalStartLine: number,
originalEndLine: number,
// original content (originalfile[originalStart...originalEnd])
originalContent: string;
newContent: string;
}
export function getDiffedLines(oldStr: string, newStr: string) {
// an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
console.debug('Line by line changes', lineByLineChanges)
lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
let oldFileLineNum: number = 0;
let newFileLineNum: number = 0;
let streakStartInNewFile: number | undefined = undefined
let streakStartInOldFile: number | undefined = undefined
let oldStrLines = oldStr.split('\n')
let newStrLines = newStr.split('\n')
const replacements: SuggestedEdit[] = []
for (let line of lineByLineChanges) {
// no change on this line
if (!line.added && !line.removed) {
// if we were on a streak, add it
if (streakStartInNewFile !== undefined) {
const startLine = streakStartInNewFile
const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
const originalStartLine = streakStartInOldFile!
const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent }
replacements.push(replacement)
streakStartInNewFile = undefined
streakStartInOldFile = undefined
}
oldFileLineNum += line.count ?? 0;
newFileLineNum += line.count ?? 0;
}
// line was removed from old file
else if (line.removed) {
// if we weren't on a streak, start one on this current line num
if (streakStartInNewFile === undefined) {
streakStartInNewFile = newFileLineNum
streakStartInOldFile = oldFileLineNum
}
oldFileLineNum += line.count ?? 0 // we processed the line so add 1
}
// line was added to new file
else if (line.added) {
// if we weren't on a streak, start one on this current line num
if (streakStartInNewFile === undefined) {
streakStartInNewFile = newFileLineNum
streakStartInOldFile = oldFileLineNum
}
newFileLineNum += line.count ?? 0; // we processed the line so add 1
}
} // end for
console.debug('Replacements', replacements)
return replacements
}
+77 -4
View File
@@ -2,18 +2,49 @@
import * as vscode from 'vscode';
import { ApiConfig } from './common/sendLLMMessage';
// a selection is a frozen snapshot
type Selection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
type File = { filepath: vscode.Uri, content: string }
// an area that is currently being diffed
type BaseDiffArea = {
// use `startLine` and `endLine` instead of `range` for mutibility
// bounds are relative to the file, inclusive
startLine: number;
endLine: number;
originalStartLine: number,
originalEndLine: number,
originalCode: string, // the original chunk of code (not necessarily the whole file)
// `newCode: string,` is not included because it is the code in the actual file, `document.text()[startline: endLine + 1]`
}
type DiffArea = BaseDiffArea & { diffareaid: number }
// the return type of diff creator
type BaseDiff = {
code: string; // representation of the diff in text
deletedRange: vscode.Range; // relative to the file, inclusive
insertedRange: vscode.Range;
deletedCode: string;
insertedCode: string;
}
// each diff on the user's screen
type Diff = {
diffid: number,
lenses: vscode.CodeLens[],
} & BaseDiff
type WebviewMessage = (
// editor -> sidebar
| { type: 'ctrl+l', selection: Selection } // user presses ctrl+l in the editor
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
// sidebar -> editor
| { type: 'applyCode', code: string } // user clicks "apply" in the sidebar
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
// sidebar -> editor
| { type: 'requestFiles', filepaths: vscode.Uri[] }
@@ -27,13 +58,55 @@ type WebviewMessage = (
// editor -> sidebar
| { type: 'apiConfig', apiConfig: ApiConfig }
// sidebar -> editor
| { type: 'getAllThreads' }
// editor -> sidebar
| { type: 'allThreads', threads: ChatThreads }
// sidebar -> editor
| { type: 'persistThread', thread: ChatThreads[string] }
// editor -> sidebar
| { type: 'startNewThread' }
// editor -> sidebar
| { type: 'toggleThreadSelector' }
)
type Command = WebviewMessage['type']
type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string;
messages: ChatMessage[];
}
}
type ChatMessage =
| {
role: "user";
content: string; // content sent to the llm
displayContent: string; // content displayed to user
selection: CodeSelection | null; // the user's selection
files: vscode.Uri[]; // the files sent in the message
}
| {
role: "assistant";
content: string; // content received from LLM
displayContent: string; // content displayed to user (this is the same as content for now)
}
export {
Selection,
BaseDiff, BaseDiffArea,
Diff, DiffArea,
CodeSelection,
File,
WebviewMessage,
Command,
ChatThreads,
ChatMessage,
}
@@ -1,160 +0,0 @@
import React, { JSX, useState } from 'react';
import { MarkedToken, Token, TokensList } from 'marked';
import { awaitVSCodeResponse, getVSCodeAPI } from './getVscodeApi';
// code block with Apply button at top
export const BlockCode = ({ text, disableApplyButton = false }: { text: string, disableApplyButton?: boolean }) => {
return <div className='py-1'>
{disableApplyButton ? null : <div className='text-sm'>
<button className='btn btn-secondary px-3 py-1 text-sm rounded-t-sm'
onClick={async () => { getVSCodeAPI().postMessage({ type: 'applyCode', code: text }) }}>Apply</button>
</div>}
<div className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${disableApplyButton ? '' : 'rounded-tl-none'}`}>
<pre className='p-3'>
{text}
</pre>
</div>
</div>
}
const Render = ({ token }: { token: Token }) => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if (t.type === "space") {
return <span>{t.raw}</span>;
}
if (t.type === "code") {
return <BlockCode text={t.text} />
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements;
return <HeadingTag>{t.text}</HeadingTag>;
}
if (t.type === "table") {
return (
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || 'left' }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td key={cellIndex} style={{ textAlign: t.align[cellIndex] || 'left' }}>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
if (t.type === "hr") {
return <hr />;
}
if (t.type === "blockquote") {
return <blockquote>{t.text}</blockquote>;
}
if (t.type === "list") {
const ListTag = t.ordered ? 'ol' : 'ul';
return (
<ListTag start={t.start !== '' ? t.start : undefined}
className={`list-inside ${t.ordered ? 'list-decimal' : 'list-disc'}`}
>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
)}
{item.text}
</li>
))}
</ListTag>
);
}
if (t.type === "paragraph") {
return <p>
{t.tokens.map((token, index) => (
<Render key={index} token={token} />
))}
</p>;
}
if (t.type === "html") {
return <pre>{`<html>`}{t.raw}{`</html>`}</pre>;
}
if (t.type === "text" || t.type === "escape") {
return <span>{t.raw}</span>;
}
if (t.type === "def") {
return null; // Definitions are typically not rendered
}
if (t.type === "link") {
return <a href={t.href} title={t.title ?? undefined}>{t.text}</a>;
}
if (t.type === "image") {
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />;
}
if (t.type === "strong") {
return <strong>{t.text}</strong>;
}
if (t.type === "em") {
return <em>{t.text}</em>;
}
// inline code
if (t.type === "codespan") {
return <code className='text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono'>{t.text}</code>;
}
if (t.type === "br") {
return <br />;
}
if (t.type === "del") {
return <del>{t.text}</del>;
}
// default
return <div className='bg-orange-50 rounded-sm overflow-hidden'>
<span className='text-xs text-orange-500'>Unknown type:</span>
{t.raw}
</div>;
};
const MarkdownRender = ({ tokens }: { tokens: TokensList }) => {
return (
<>
{tokens.map((token, index) => (
<Render key={index} token={token} />
))}
</>
);
};
export default MarkdownRender;
+140 -143
View File
@@ -1,12 +1,15 @@
import React, { useState, ChangeEvent, useEffect, useRef, useCallback, FormEvent } from "react"
import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage"
import { Command, File, Selection, WebviewMessage } from "../shared_types"
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"
import { File, CodeSelection, WebviewMessage, ChatMessage } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
import { marked } from 'marked';
import MarkdownRender, { BlockCode } from "./MarkdownRender";
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import * as vscode from 'vscode'
import { SelectedFiles } from "./components/SelectedFiles";
import { useThreads } from "./threadsContext";
const filesStr = (fullFiles: File[]) => {
@@ -18,7 +21,7 @@ ${content}
\`\`\``).join('\n')
}
const userInstructionsStr = (instructions: string, files: File[], selection: Selection | null) => {
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
return `
${filesStr(files)}
@@ -27,7 +30,7 @@ I am currently selecting this code:
\`\`\`${selection.selectionStr}\`\`\`
`}
Please edit the code following these instructions:
Please edit the code following these instructions (or, if appropriate, answer my question instead):
${instructions}
If you make a change, rewrite the entire file.
@@ -35,40 +38,6 @@ If you make a change, rewrite the entire file.
}
const FilesSelector = ({ files, setFiles }: { files: vscode.Uri[], setFiles: (files: vscode.Uri[]) => void }) => {
return files.length !== 0 && <div className='my-2'>
Include files:
{files.map((filename, i) =>
<div key={i} className='flex'>
{/* X button on a file */}
<button type='button' onClick={() => {
let file_index = files.indexOf(filename)
setFiles([...files.slice(0, file_index), ...files.slice(file_index + 1, Infinity)])
}}>
-{' '}<span className='text-gray-500'>{getBasename(filename.fsPath)}</span>
</button>
</div>
)}
</div>
}
const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => {
return files.length !== 0 && <div className='text-xs my-2'>
{files.map((filename, i) =>
<div key={i} className='flex'>
<button type='button'
className='btn btn-secondary pointer-events-none'
onClick={() => {
// TODO redirect to the document filename.fsPath, when add this remove pointer-events-none
}}>
-{' '}<span className='text-gray-100'>{getBasename(filename.fsPath)}</span>
</button>
</div>
)}
</div>
}
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
const role = chatMessage.role
@@ -81,14 +50,14 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
if (role === 'user') {
chatbubbleContents = <>
<IncludedFiles files={chatMessage.files} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} disableApplyButton={true} />}
<SelectedFiles files={chatMessage.files} setFiles={null} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
{children}
</>
}
else if (role === 'assistant') {
const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer
chatbubbleContents = <MarkdownRender tokens={tokens} /> // sectionsHTML
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
}
@@ -99,51 +68,58 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
</div>
}
const getBasename = (pathStr: string) => {
// "unixify" path
pathStr = pathStr.replace(/[/\\]+/g, '/'); // replace any / or \ or \\ with /
const parts = pathStr.split('/') // split on /
return parts[parts.length - 1]
}
type ChatMessage = {
role: 'user'
content: string, // content sent to the llm
displayContent: string, // content displayed to user
selection: Selection | null, // the user's selection
files: vscode.Uri[], // the files sent in the message
} | {
role: 'assistant',
content: string, // content received from LLM
displayContent: string // content displayed to user (this is the same as content for now)
}
// const [stateRef, setState] = useInstantState(initVal)
// setState instantly changes the value of stateRef instead of having to wait until the next render
const useInstantState = <T,>(initVal: T) => {
const stateRef = useRef<T>(initVal)
const [_, setS] = useState<T>(initVal)
const setState = useCallback((newVal: T) => {
setS(newVal);
stateRef.current = newVal;
}, [])
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
const ThreadSelector = ({ onClose }: { onClose: () => void }) => {
const { allThreads, currentThread, switchToThread } = useThreads()
return (
<div className="flex flex-col space-y-1">
<div className="text-right">
<button className="btn btn-sm" onClick={onClose}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* iterate through all past threads */}
{Object.keys(allThreads ?? {}).map((threadId) => {
const pastThread = (allThreads ?? {})[threadId];
return (
<button
key={pastThread.id}
className={`btn btn-sm btn-secondary ${pastThread.id === currentThread?.id ? "btn-primary" : ""}`}
onClick={() => switchToThread(pastThread.id)}
>
{new Date(pastThread.createdAt).toLocaleString()}
</button>
)
})}
</div>
)
}
const Sidebar = () => {
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
// state of current message
const [selection, setSelection] = useState<Selection | null>(null) // the code the user is selecting
const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
const [instructions, setInstructions] = useState('') // the user's instructions
// state of chat
const [chatMessageHistory, setChatHistory] = useState<ChatMessage[]>([])
const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null)
@@ -165,13 +141,12 @@ const Sidebar = () => {
// if user pressed ctrl+l, add their selection to the sidebar
if (m.type === 'ctrl+l') {
setSelection(m.selection)
const filepath = m.selection.filePath
// add file if it's not a duplicate
if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, filepath])
// add current file to the context if it's not already in the files array
if (!files.find(f => f.fsPath === filepath.fsPath))
setFiles(files => [...files, filepath])
}
// when get apiConfig, set
@@ -179,10 +154,22 @@ const Sidebar = () => {
setApiConfig(m.apiConfig)
}
// if they pressed the + to add a new chat
else if (m.type === 'startNewThread') {
setIsThreadSelectorOpen(false)
if (currentThread?.messages.length !== 0)
startNewThread()
}
// if they opened thread selector
else if (m.type === 'toggleThreadSelector') {
setIsThreadSelectorOpen(v => !v)
}
}
window.addEventListener('message', listener);
return () => { window.removeEventListener('message', listener) }
}, [files, selection])
}, [files, selection, startNewThread, currentThread])
const formRef = useRef<HTMLFormElement | null>(null)
@@ -205,17 +192,16 @@ const Sidebar = () => {
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
// console.log('prompt:\n', content)
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
addMessageToHistory(newHistoryElt)
// send message to claude
// send message to LLM
let { abort } = sendLLMMessage({
messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
onText: (newText, fullText) => setMessageStream(fullText),
onFinalMessage: (content) => {
// add assistant's message to chat history
// add assistant's message to chat history, and clear selection
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
addMessageToHistory(newHistoryElt)
// clear selection
setMessageStream('')
@@ -234,12 +220,12 @@ const Sidebar = () => {
// if messageStream was not empty, add it to the history
const llmContent = messageStream || '(canceled)'
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
}, [messageStream])
}, [addMessageToHistory, messageStream])
//Clear code selection
const clearSelection = () => {
@@ -248,9 +234,14 @@ const Sidebar = () => {
return <>
<div className="flex flex-col h-screen w-full">
{isThreadSelectorOpen && (
<div className="mb-2 max-h-[30vh] overflow-y-auto">
<ThreadSelector onClose={() => setIsThreadSelectorOpen(false)} />
</div>
)}
<div className="overflow-y-auto overflow-x-hidden space-y-4">
{/* previous messages */}
{chatMessageHistory.map((message, i) =>
{currentThread !== null && currentThread.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
@@ -260,60 +251,66 @@ const Sidebar = () => {
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
{/* selected files */}
<FilesSelector files={files} setFiles={setFiles} />
{/* selected code */}
{!selection?.selectionStr ? null
: (
<div className="relative">
<button
onClick={clearSelection}
className="absolute top-2 right-2 text-white hover:text-gray-300 z-10"
>
X
</button>
<BlockCode text={selection.selectionStr} disableApplyButton={true} />
</div>
)}
<div className="relative">
<div className="input">
{/* selection */}
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
{/* selected files */}
<SelectedFiles files={files} setFiles={setFiles} />
{/* selected code */}
{!!selection?.selectionStr && (
<BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
<button
onClick={clearSelection}
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
>
Remove
</button>
)} />
)}
</div>}
<form
ref={formRef}
className="flex flex-row items-center rounded-md p-2"
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
onSubmit={(e) => {
console.log('submit!')
e.preventDefault();
onSubmit(e)
}}>
{/* input */}
<textarea
onChange={(e) => { setInstructions(e.target.value) }}
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
placeholder="Ctrl+L to select"
rows={1}
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
/>
{/* submit button */}
{isLoading ?
<button
onClick={onStop}
className="btn btn-primary rounded-r-lg max-h-10 p-2"
type='button'
>Stop</button>
: <button
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={!instructions}
type='submit'
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
}
</form>
</div>
</div>
</div>
<form
ref={formRef}
className="flex flex-row items-center rounded-md p-2 input"
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
onSubmit={(e) => {
console.log('submit!')
e.preventDefault();
onSubmit(e)
}}>
{/* input */}
<textarea
onChange={(e) => { setInstructions(e.target.value) }}
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
placeholder="Ctrl+L to select"
rows={1}
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
/>
{/* submit button */}
{isLoading ?
<button
onClick={onStop}
className="btn btn-primary rounded-r-lg max-h-10 p-2"
type='button'
>Stop</button>
: <button
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={!instructions}
type='submit'
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
}
</form>
</div>
</div>
@@ -0,0 +1,48 @@
import React from "react"
import * as vscode from "vscode"
const getBasename = (pathStr: string) => {
// "unixify" path
pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
const parts = pathStr.split("/") // split on /
return parts[parts.length - 1]
}
export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
return (
files.length !== 0 && (
<div className="flex flex-wrap -mx-1 -mb-1">
{files.map((filename, i) => (
<button
key={filename.path}
disabled={!setFiles}
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
type="button"
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
>
<span>{getBasename(filename.fsPath)}</span>
{/* X button */}
{!!setFiles && <span className="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</span>}
</button>
))}
</div>
)
)
}
+7 -2
View File
@@ -5,11 +5,16 @@ import { Command, WebviewMessage } from "../shared_types";
// message -> res[]
const awaiting: { [c in Command]: ((res: any) => void)[] } = {
"ctrl+l": [],
"applyCode": [],
"applyChanges": [],
"requestFiles": [],
"files": [],
"apiConfig": [],
"getApiConfig": []
"getApiConfig": [],
"startNewThread": [],
"getAllThreads": [],
"allThreads": [],
"persistThread": [],
"toggleThreadSelector": []
}
// use this function to await responses
+15 -11
View File
@@ -1,16 +1,20 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import Sidebar from './Sidebar'
import * as React from "react"
import * as ReactDOM from "react-dom/client"
import Sidebar from "./Sidebar"
import { ThreadsProvider } from "./threadsContext"
// mount the sidebar on the id="root" element
if (typeof document === 'undefined') {
console.log('index.tsx error: document was undefined')
if (typeof document === "undefined") {
console.log("index.tsx error: document was undefined")
}
const rootElement = document.getElementById('root')!
console.log('root Element', rootElement)
const rootElement = document.getElementById("root")!
console.log("Void root Element:", rootElement)
const extension = (
<ThreadsProvider>
<Sidebar />
</ThreadsProvider>
)
const root = ReactDOM.createRoot(rootElement)
root.render(<Sidebar />)
root.render(extension)
@@ -0,0 +1,80 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { getVSCodeAPI } from "../getVscodeApi"
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000
// code block with toolbar (Apply, Copy, etc) at top
const BlockCode = ({
text,
toolbar,
hideToolbar = false,
className,
}: {
text: string
toolbar?: ReactNode
hideToolbar?: boolean
className?: string
}) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
}, COPY_FEEDBACK_TIMEOUT)
}
}, [copyButtonState])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(
() => {
setCopyButtonState(CopyButtonState.Copied)
},
() => {
setCopyButtonState(CopyButtonState.Error)
}
)
}, [text])
const defaultToolbar = (
<>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={onCopy}
>
{copyButtonState}
</button>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
getVSCodeAPI().postMessage({ type: "applyChanges", code: text })
}}
>
Apply
</button>
</>
)
return (
<div className="relative group">
{!hideToolbar && (
<div className="absolute top-0 right-0 invisible group-hover:visible">
<div className="flex space-x-2 p-2">{toolbar || defaultToolbar}</div>
</div>
)}
<div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
>
<pre className="p-2">{text}</pre>
</div>
</div>
)
}
export default BlockCode
@@ -0,0 +1,168 @@
import React, { JSX } from "react"
import { marked, MarkedToken, Token, TokensList } from "marked"
import BlockCode from "./BlockCode"
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if (t.type === "space") {
return <span>{t.raw}</span>
}
if (t.type === "code") {
return <BlockCode text={t.text} />
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
return <HeadingTag>{t.text}</HeadingTag>
}
if (t.type === "table") {
return (
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
style={{ textAlign: t.align[cellIndex] || "left" }}
>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
if (t.type === "hr") {
return <hr />
}
if (t.type === "blockquote") {
return <blockquote>{t.text}</blockquote>
}
if (t.type === "list") {
const ListTag = t.ordered ? "ol" : "ul"
return (
<ListTag
start={t.start ? t.start : undefined}
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
)}
<MarkdownRender string={item.text} nested={true} />
</li>
))}
</ListTag>
)
}
if (t.type === "paragraph") {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index} token={token} />
))}
</>
if (nested)
return contents
return <p>{contents}</p>
}
// don't actually render <html> tags, just render strings of them
if (t.type === "html") {
return (
<pre>
{`<html>`}
{t.raw}
{`</html>`}
</pre>
)
}
if (t.type === "text" || t.type === "escape") {
return <span>{t.raw}</span>
}
if (t.type === "def") {
return <></> // Definitions are typically not rendered
}
if (t.type === "link") {
return (
<a href={t.href} title={t.title ?? undefined}>
{t.text}
</a>
)
}
if (t.type === "image") {
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
}
if (t.type === "strong") {
return <strong>{t.text}</strong>
}
if (t.type === "em") {
return <em>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
return (
<code className="text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono">
{t.text}
</code>
)
}
if (t.type === "br") {
return <br />
}
// strikethrough
if (t.type === "del") {
return <del>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden">
<span className="text-xs text-orange-500">Unknown type:</span>
{t.raw}
</div>
)
}
const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} />
))}
</>
)
}
export default MarkdownRender
+22 -18
View File
@@ -3,33 +3,37 @@
@tailwind utilities;
html {
font-size: var(--vscode-font-size);
font-size: var(--vscode-font-size);
}
.btn {
@apply cursor-pointer transition-colors;
@apply cursor-pointer transition-colors;
&.btn-primary {
@apply bg-vscode-button-bg text-vscode-button-fg;
&.btn-primary {
@apply bg-vscode-button-bg text-vscode-button-fg;
&:not(:disabled) {
@apply hover:bg-vscode-button-hoverBg;
}
}
&:not(:disabled) {
@apply hover:bg-vscode-button-hoverBg;
}
}
&.btn-secondary {
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
&.btn-sm {
@apply px-3 py-1 text-sm;
}
&:not(:disabled) {
@apply hover:bg-vscode-button-secondary-hoverBg;
}
}
&.btn-secondary {
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
&:not(:disabled) {
@apply hover:bg-vscode-button-secondary-hoverBg;
}
}
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
}
.input {
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
}
@@ -0,0 +1,100 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { ChatMessage, ChatThreads } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
type ThreadsContextValue = {
readonly allThreads: ChatThreads | null,
readonly currentThread: ChatThreads[string] | null;
addMessageToHistory: (message: ChatMessage) => void;
switchToThread: (threadId: string) => void;
startNewThread: () => void;
}
const ThreadsContext = createContext<ThreadsContextValue>(undefined as unknown as ThreadsContextValue)
const createNewThread = () => ({
id: new Date().getTime().toString(),
createdAt: new Date().toISOString(),
messages: [],
})
// const [stateRef, setState] = useInstantState(initVal)
// setState instantly changes the value of stateRef instead of having to wait until the next render
const useInstantState = <T,>(initVal: T) => {
const stateRef = useRef<T>(initVal)
const [_, setS] = useState<T>(initVal)
const setState = useCallback((newVal: T) => {
setS(newVal);
stateRef.current = newVal;
}, [])
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
}
export function ThreadsProvider({ children }: { children: ReactNode }) {
const [allThreads, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadId, setCurrentThreadId] = useInstantState<string | null>(null)
// this loads allThreads in on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: "getAllThreads" })
awaitVSCodeResponse('allThreads')
.then(response => {
setAllThreads(response.threads)
})
}, [setAllThreads])
return (
<ThreadsContext.Provider
value={{
allThreads: allThreads.current,
currentThread: currentThreadId.current === null || allThreads.current === null ? null : allThreads.current[currentThreadId.current],
addMessageToHistory: (message: ChatMessage) => {
let currentThread: ChatThreads[string]
if (!(currentThreadId.current === null || allThreads.current === null)) {
currentThread = allThreads.current[currentThreadId.current]
}
else {
currentThread = createNewThread()
setCurrentThreadId(currentThread.id)
}
setAllThreads({
...allThreads.current,
[currentThread.id]: {
...currentThread,
messages: [...currentThread.messages, message],
}
})
getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread })
},
switchToThread: (threadId: string) => {
setCurrentThreadId(threadId);
},
startNewThread: () => {
const newThread = createNewThread()
setAllThreads({
...allThreads.current,
[newThread.id]: newThread
})
setCurrentThreadId(newThread.id)
},
}}
>
{children}
</ThreadsContext.Provider>
)
}
export function useThreads(): ThreadsContextValue {
const context = useContext<ThreadsContextValue>(ThreadsContext)
if (context === undefined) {
throw new Error("useThreads must be used within a ThreadsProvider")
}
return context
}
+13 -11
View File
@@ -7,17 +7,19 @@ module.exports = {
extend: {
colors: {
vscode: {
'editor-bg': "var(--vscode-editor-background)",
'editor-fg': "var(--vscode-editor-foreground)",
'input-bg': "var(--vscode-input-background)",
'input-fg': "var(--vscode-input-foreground)",
'input-border': "var(--vscode-input-border)",
'button-fg': "var(--vscode-button-foreground)",
'button-bg': "var(--vscode-button-background)",
'button-hoverBg': "var(--vscode-button-hoverBackground)",
'button-secondary-fg': "var(--vscode-button-secondaryForeground)",
'button-secondary-bg': "var(--vscode-button-secondaryBackground)",
'button-secondary-hoverBg': "var(--vscode-button-secondaryHoverBackground)",
"sidebar-bg": "var(--vscode-sideBar-background)",
"editor-bg": "var(--vscode-editor-background)",
"editor-fg": "var(--vscode-editor-foreground)",
"input-bg": "var(--vscode-input-background)",
"input-fg": "var(--vscode-input-foreground)",
"input-border": "var(--vscode-input-border)",
"button-fg": "var(--vscode-button-foreground)",
"button-bg": "var(--vscode-button-background)",
"button-hoverBg": "var(--vscode-button-hoverBackground)",
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
"button-secondary-hoverBg":
"var(--vscode-button-secondaryHoverBackground)",
},
},
},
+8
View File
@@ -137,6 +137,7 @@
"mocha-multi-reporters": "^1.5.1",
"npm-run-all": "^4.1.5",
"opn": "^6.0.0",
"original-fs": "^1.2.0",
"os-browserify": "^0.3.0",
"p-all": "^1.0.0",
"path-browserify": "^1.0.1",
@@ -14203,6 +14204,13 @@
"util-deprecate": "~1.0.1"
}
},
"node_modules/original-fs": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/original-fs/-/original-fs-1.2.0.tgz",
"integrity": "sha512-IGo+qFumpIV65oDchJrqL0BOk9kr82fObnTesNJt8t3YgP6vfqcmRs0ofPzg3D9PKMeBHt7lrg1k/6L+oFdS8g==",
"dev": true,
"license": "Unlicense"
},
"node_modules/os-browserify": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+1
View File
@@ -199,6 +199,7 @@
"mocha-multi-reporters": "^1.5.1",
"npm-run-all": "^4.1.5",
"opn": "^6.0.0",
"original-fs": "^1.2.0",
"os-browserify": "^0.3.0",
"p-all": "^1.0.0",
"path-browserify": "^1.0.1",
Regular → Executable
View File