Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ffab60b4b8 | |||
| 2309fc53fb | |||
| e153ffee4e | |||
| a3d741d685 | |||
| a12e35c5f0 | |||
| 5f3e664eed | |||
| fb69afd5c4 | |||
| 8c9eeaa133 | |||
| 00f59e9555 | |||
| 767c45080f | |||
| b0758e02aa | |||
| 7f81836d9f | |||
| 33c3ef6751 | |||
| 20056f209e | |||
| 69d848f22b | |||
| 1557a44934 | |||
| 89dd63a0ee | |||
| e5c55145e9 | |||
| 2638d62a24 | |||
| e04d839640 | |||
| 0158e2fd5d | |||
| 3e308f2082 | |||
| 429224993b | |||
| 1b8271d217 | |||
| 29ef6b5b43 | |||
| 23a42659b3 | |||
| 9d7a5eccd3 | |||
| 42bdcf54ca | |||
| 251b65d04d | |||
| b72da01632 | |||
| e2f337f882 | |||
| 496c0178de | |||
| 38d31ae737 | |||
| d3beafec74 | |||
| 73fb5bb3c6 | |||
| 247a3b7feb | |||
| 2e5be73f0c | |||
| 2ca1db35af | |||
| 11baebdf54 | |||
| 8dde88222c | |||
| 2b47e9745f | |||
| f96416be38 | |||
| a4ef0de0ac | |||
| f7a905e27f | |||
| c579bcc2fd | |||
| ad7b7a6866 | |||
| b07842ec3f | |||
| 19b2b8bd77 | |||
| f4a407d80f | |||
| 95be6e0b37 | |||
| 6741a6fceb | |||
| db560744fb | |||
| 8937b52e72 | |||
| 7550b96389 | |||
| 8aa3282c3d | |||
| 9ed68d5fac | |||
| 7668d3dee7 | |||
| fc65d847c4 | |||
| 653d5e9c1f | |||
| 631f724138 | |||
| 162bf704cc | |||
| d64e9c93f5 | |||
| 74c610632e | |||
| 2496a2c42e | |||
| 656b24b701 | |||
| 3fd9b572fe | |||
| dab75a6614 | |||
| 4747691e74 | |||
| 6dac9626c0 | |||
| 180df75bb4 | |||
| 5039ca8fdf | |||
| aea35c95f2 | |||
| 0a5ebc1c50 | |||
| cbe3d4bd45 | |||
| 16b84ec933 | |||
| 2661f5e8e1 | |||
| 760f8c447c | |||
| 57db928f7a | |||
| 9b130edcfe | |||
| ff0dcd5095 | |||
| 30b528402c | |||
| 6d513a1e60 | |||
| 7269c4e582 | |||
| 083bbf7d9f | |||
| e54f49ebfb | |||
| 31cd10b9e6 | |||
| c6dcb8eb9e | |||
| 3a98d1edb1 | |||
| 22d32aca3a | |||
| fe5e02c3a9 | |||
| 3d58627f30 | |||
| 20fd8f318b | |||
| 98ee94018e | |||
| ac9bc80e20 | |||
| 8203b42169 | |||
| bc027b2fca | |||
| afa24231e6 | |||
| da5bb4b68e | |||
| f2d741d01f | |||
| e893dc0327 | |||
| e05a399177 | |||
| 4f285ce364 | |||
| 2be5a2d13a | |||
| 5e5fe80901 | |||
| 44bc8bfdbf | |||
| 4f9b5ac873 | |||
| 1beb011bfa | |||
| bf9029f9bc | |||
| d7a3c44e31 | |||
| 7b31f2841a | |||
| e8cf6ee9e7 | |||
| 2b3f828654 | |||
| 7ee1840e77 | |||
| 1843cd0878 | |||
| 78e10a5593 | |||
| 7bcefd4536 | |||
| 6d5f885306 | |||
| 99adb23149 | |||
| 043a38bd36 | |||
| 0269411c16 | |||
| f4ceb53738 |
+18
-5
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Generated
+23
-422
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
// };
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
// }
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Generated
+8
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user