Compare commits

...

98 Commits

Author SHA1 Message Date
Andrew a29543e4a7 Promisify 2024-10-25 01:33:54 -07:00
Andrew d4a2ac2796 draft 2024-10-25 01:22:10 -07:00
Andrew 4b5e09c124 Merge remote-tracking branch 'origin/speculative-edits' into ctrlk-diff-ui 2024-10-24 21:08:57 -07:00
Andrew f8f48fb98b + 2024-10-24 20:48:04 -07:00
Andrew 1b8e7f3643 amend 2024-10-24 19:59:25 -07:00
Andrew 420ecb320e Merge branch 'main' into speculative-edits 2024-10-24 19:58:58 -07:00
mp 097f279ecb Throttle edit requests, refactor 2024-10-24 19:48:31 -07:00
Andrew 9a1d4e2b9c comment 2024-10-24 19:34:09 -07:00
Andrew a926efcd2a attempt windows build 2024-10-24 19:25:20 -07:00
Andrew Pareles 40aa319d18 update react comment 2024-10-24 15:37:13 -07:00
Andrew Pareles 549dabbde6 X [ERROR] Cannot start service: Host version "0.23.1" 2024-10-24 15:35:43 -07:00
Andrew Pareles 42e52647f3 Merge pull request #121 from jcommaret/main
Doc : Build command fix
2024-10-23 11:33:09 -07:00
Andrew Pareles a12ffd6b5e Update CONTRIBUTING.md 2024-10-23 11:32:52 -07:00
Andrew Pareles 3d74e77760 Update CONTRIBUTING.md 2024-10-23 11:32:27 -07:00
Jérôme Commaret 0ffd66e747 Doc : Build command fix 2024-10-23 12:30:57 +02:00
Andrew Pareles e78fa459cb Merge pull request #120 from voideditor/@andrewpareles/editor-insets
discord
2024-10-23 03:17:52 -07:00
Andrew c01b1e9488 discord 2024-10-23 03:16:10 -07:00
Andrew Pareles 45c7b951e9 or just run 2024-10-23 01:48:39 -07:00
Andrew Pareles f31f5e7ad1 Merge pull request #119 from voideditor/@andrewpareles/editor-insets
Editor Insets
2024-10-23 01:46:54 -07:00
Andrew Pareles c3a934777d Update CONTRIBUTING.md 2024-10-23 01:46:30 -07:00
Andrew Pareles 3aba62a6ed update contrib + ctrlk hello world works! 2024-10-23 01:40:47 -07:00
Andrew Pareles ad5128f885 amend 2024-10-23 00:51:31 -07:00
Andrew Pareles 376172e5a9 update contributing 2024-10-23 00:49:33 -07:00
Andrew Pareles 4dd6553e34 building on a mac works! so does editorInset! 2024-10-22 23:49:03 -07:00
Andrew f2b3a2be30 comment out checkProposedApiEnabled(extension, 'editorInsets'); 2024-10-22 22:59:09 -07:00
mp 1e4c932313 Track call time 2024-10-22 14:32:39 -07:00
mp 7bd9e01a15 Working draft of speculative edits 2024-10-21 18:33:00 -07:00
Andrew 69277a287a update Ollama message 2024-10-21 15:24:53 -07:00
Andrew Pareles 6c0f17d8f9 Merge pull request #113 from voideditor/monaco-editor
Misc Improvements
2024-10-21 14:45:30 -07:00
Andrew 6dc9988af0 amend 2024-10-20 23:28:51 -07:00
Andrew 18656372f8 add ctrlk, autofocus chatbar 2024-10-20 23:24:40 -07:00
Andrew febf492790 clarify 2024-10-20 20:15:25 -07:00
Andrew bcb0c33616 amend 2024-10-20 19:23:47 -07:00
Andrew Pareles 7072933a88 Update build-tsx.js 2024-10-20 19:20:03 -07:00
Andrew f2342bee2d add back syntax highlighter 2024-10-20 18:39:53 -07:00
Andrew 572e9f07ea misc 2024-10-20 17:14:10 -07:00
Andrew 09d84044b5 monaco loads... need to adjust height 2024-10-19 21:38:17 -07:00
mp 9ecf596cbb Prepare to debug 2024-10-19 20:17:54 -07:00
Andrew a49a1324a5 Merge branch 'main' into monaco-editor 2024-10-19 18:50:05 -07:00
Andrew 8538caeffc start adding monaco 2024-10-19 18:49:43 -07:00
Andrew 79ecd98818 add metrics 2024-10-19 18:21:19 -07:00
mp f6cad40a44 Finish speculative draft 2024-10-18 19:18:11 -07:00
mp e25368ef7f Merge branch 'main' into speculative-edits 2024-10-18 17:25:13 -07:00
mp 7ce17a0854 Initial draft for speculative edits 2024-10-18 17:19:38 -07:00
Andrew c5b5e07a98 monaco comment 2024-10-18 00:48:44 -07:00
Andrew 2addd06e60 switch to new tab on ctrl+l 2024-10-17 23:26:31 -07:00
Andrew 177f2e92f8 amend - fix disabled 2024-10-17 21:34:30 -07:00
Andrew 217f61902e add metrics 2024-10-17 21:23:07 -07:00
Andrew 9274602051 amend - uuid type 2024-10-17 19:15:42 -07:00
Andrew Pareles efa3b033af Merge pull request #110 from voideditor/ui-refactor-10-15
Settings update
2024-10-17 17:31:26 -07:00
Andrew 18649d3255 fix package 2024-10-17 17:29:28 -07:00
Andrew 2b55d94cce Merge branch 'main' into ui-refactor-10-15 2024-10-17 17:26:08 -07:00
Andrew Pareles 2d17133684 Merge pull request #87 from w1gs/main
Added displaying error messages in UI
2024-10-17 17:18:56 -07:00
Andrew e2e6cb0327 Merge branch 'main' into pr/w1gs/87 2024-10-17 17:18:12 -07:00
Andrew 5415f1e3cb amend 2024-10-17 16:46:52 -07:00
Andrew 0f3a79ab54 error UI 2024-10-17 16:43:25 -07:00
Andrew 983e926a23 add error handling from #87 2024-10-17 16:36:37 -07:00
Andrew 302aa2b384 warning for devs 2024-10-17 15:30:01 -07:00
Andrew 84ff9784ca fix sidebar settings 2024-10-17 01:20:59 -07:00
Andrew Pareles 6dfab6d00c Merge pull request #77 from SOUMITRO-SAHA/soumitrasaha#ui
Fix: Add Syntax Highlighting to Sidebar Code Snippets
2024-10-17 00:17:58 -07:00
Andrew 628ed9a578 Merge branch 'main' into pr/SOUMITRO-SAHA/77 2024-10-17 00:15:15 -07:00
Andrew 0a6f7b8ebc Merge branch 'main' into ui-refactor-10-15 2024-10-17 00:12:03 -07:00
Andrew Pareles 5b511edca1 Merge pull request #108 from IDLe-Engineering/patch-3
Connect-src updates for OpenAICompatible
2024-10-17 00:11:25 -07:00
Andrew Pareles 4ecbd998bd Merge pull request #106 from IDLe-Engineering/patch-2
Build prompt dynamically
2024-10-17 00:10:54 -07:00
Andrew fcd0eb30c6 fix 2024-10-16 23:06:34 -07:00
Andrew aa4ce3478a update styles in settings page 2024-10-16 23:03:51 -07:00
Andrew 1e82781c52 outline color 2024-10-16 20:42:47 -07:00
Andrew edf9dc9279 thread switching 2024-10-16 20:03:50 -07:00
Andrew 63c6b6bec5 vscode styles 2024-10-16 20:00:07 -07:00
Andrew 154a82ab0b add settings UI 2024-10-16 19:32:21 -07:00
Andrew Pareles ab51e97008 Merge pull request #109 from IDLe-Engineering/patch-4
Add Syntax Highlighting to Sidebar Code Snippets
2024-10-16 16:23:18 -07:00
Andrew Pareles 0240fc1803 Update package.json 2024-10-16 16:22:59 -07:00
Andrew d3b862ae91 history display slightly nicer 2024-10-16 16:19:32 -07:00
Andrew dc2344fd4c Merge branch 'main' into ui-refactor-10-15 2024-10-16 14:53:03 -07:00
Andrew 0072c554fb tabs 2024-10-16 14:52:49 -07:00
Tom Spielvogel ed8381c6cb Add Syntax Highlighting to Sidebar Code Snippets 2024-10-16 13:29:46 +02:00
Tom Spielvogel 40b4ecb4f8 Add Syntax Highlighting to Sidebar Code Snippets 2024-10-16 13:29:27 +02:00
Tom Spielvogel 5268ededbb Add Syntax Highlighting to Sidebar Code Snippets 2024-10-16 13:28:48 +02:00
Tom Spielvogel a69e1b940f Add Syntax Highlighting to Sidebar Code Snippets 2024-10-16 13:26:24 +02:00
Tom Spielvogel 15994c46d1 Adapted to new type CodeSelection 2024-10-16 11:28:23 +02:00
Tom Spielvogel b565a91194 Connect-src needs hold the entire path for chat completions for OpenAICompatible
Simply using the enpoint will trigger a Content-Security-Policy error in vscode when trying to send messages to the LLM
2024-10-16 10:56:46 +02:00
Andrew b30937934a progress moving settings to be extension-native 2024-10-16 01:12:57 -07:00
Tom Spielvogel eec7cd7321 Update SidebarChat.tsx
Build the prompt based on the actual use-case.
If no files or code is selected, don't prompt the LLM to change it. It saves time and tokens
2024-10-16 08:55:37 +02:00
Andrew Pareles f86b84dcc6 Merge pull request #105 from voideditor/ui-refactor-10-15
Split up Sidebar tabs into files and clearer message types (incorporates direction)
2024-10-15 20:54:33 -07:00
Andrew cd9bca0451 split up message direction and Sidebar tabs 2024-10-15 20:51:46 -07:00
Darion ce3043b595 Merge branch 'main' into main 2024-10-15 21:36:00 -04:00
Andrew Pareles 68c5ebb000 Merge pull request #101 from voideditor/diff-selections
Refactor and fix diff highlighting
2024-10-15 15:57:33 -07:00
w1gs 08e91e921b style fixes 2024-10-15 12:36:38 -04:00
w1gs 6c2ca794f5 fixed merge conflicts 2024-10-15 12:24:26 -04:00
Darion b3fd408cf7 Merge branch 'main' into main 2024-10-15 11:09:32 -04:00
Andrew Pareles dc96d126bb add package.json to list of void's changed files 2024-10-15 00:31:40 -07:00
Andrew 07fd622901 add OpenRouter - Fixes Add OpenRouter hosting option #20 2024-10-14 23:50:34 -07:00
Andrew bb74cbb56b swap 2024-10-14 23:32:25 -07:00
Darion ef94de48c2 Merge branch 'main' into main 2024-10-11 12:56:08 -04:00
w1gs 14a8b0ee4e fixed merge conflicts 2024-10-11 12:52:12 -04:00
Darion c3e2dc1621 Merge branch 'voideditor:main' into main 2024-10-05 16:53:40 -04:00
w1gs bed7929e48 Added error messages in UI 2024-10-03 21:24:53 -04:00
SOUMITRO-SAHA b8a5e5b67c Fix: Add Syntax Highlighting to Sidebar Code Snippets 2024-09-29 20:49:59 +05:30
33 changed files with 4437 additions and 1596 deletions
+55 -60
View File
@@ -12,6 +12,7 @@ We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-f
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
## 1. Building the Extension
Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
1. Clone the repository:
@@ -20,11 +21,7 @@ Here's how you can start contributing to the Void extension. This is where you s
git clone https://github.com/voideditor/void
```
2. Open the folder `/extensions/void` in VS Code (open it in a new workspace, *don't* just cd into it):
```
open /extensions/void
```
2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, _don't_ just cd into it).
3. Install dependencies:
@@ -32,7 +29,7 @@ open /extensions/void
npm install
```
4. Build the project. We created this build command so that we could run React in vscode - it converts `sidebar/index.tsx` into a CSS/JS bundle in `dist/`.
4. Compile the React by running `npm run build`. We created this build command to convert `sidebar/index.tsx` into `dist/`.
```
npm run build
@@ -40,19 +37,15 @@ npm run build
5. Run the project by pressing <kbd>F5</kbd>.
This will start a new instance of VS Code with the extension enabled. If this does not work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VS Code Extension Development".
If you would like to use AI features, you need to provide an API key. You can do that by going to Settings (Ctrl+,) typing in "void", and adding the API key you want to use (eg. the `"Anthropic Api Key"` environment variable). The "Which API" environment variable controls the provider and defaults to "anthropic".
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
This will start a new instance of VSCode with the extension enabled. If this doesn't work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VSCode Extension Development".
## 2. Building the full IDE
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.
If you want to work on the full IDE, please follow the steps below. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page, which is where the steps below come from. Also feel free to submit an issue or get in touch with us with any build errors.
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.
### a. Building on a Mac
Make sure you're on the correct NodeJS version as per `.nvmrc`.
To build on a Mac, open `void/` in VSCode. Make sure you've built the extension by following the steps above (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`). Also make sure you have Python and XCode installed on your system (you probably do by default).
1. Install all dependencies.
@@ -60,64 +53,72 @@ Make sure you're on the correct NodeJS version as per `.nvmrc`.
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.
2. Run `npm run watch`.
3. Run `./scripts/code.sh` in your terminal.
This can take ~5 min. It's done when you see something like:
```
[watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms
[watch-client ] [00:38:06] Finished compilation with 0 errors after 46248 ms
[watch-client ] [00:38:07] Starting compilation...
[watch-client ] [00:38:07] Finished compilation with 0 errors after 5 ms
```
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
3. In a new terminal, run `./scripts/code.sh`.
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-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.
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
**Common Fixes:**
- Make sure you have the same NodeJS version as `.nvmrc`.
- If you see `X [ERROR] Cannot start service: Host version "0.23.1" does not match binary version "0.23.0"`, run `npm i -D esbuild@0.23.0`
### b. Building on Windows
To build on Windows, please refer to [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). We recommend building on Mac; we're Windows users who switch to Mac to build right now.
<!-- Get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community). Also find the boxes for "Desktop development with C++" and "Node.js development" and get those, too.
If you get a node-gyp error in the next few steps, you should also get [Visual Studio Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools), find the Visual Studio Build Tools box, click Install (or Modify), then in Individual Components:
check every item under `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`, `C++ ATL for latest build tools with Spectre Mitigations`, and `C++ MFC for latest build tools with Spectre Mitigations`.
```
npm config set msvs_version 2022
```
-->
## Roadmap
Here are the most important topics on our Roadmap. More ⭐'s = more important.
## ⭐⭐⭐ Improve diffs.
We define a "diff" as a single green/red pair that denotes a change. Here are improvements to make:
1. Show deletion (-) diffs. Right now we're only showing insertion (+) diffs. Diffs currently work by highlighting all of the new code in green with a simple text decoration. Instead, we would like to use code from VS Code's native diffEditor to show the diffs ("inline" mode). We could alternatively keep what we have and add red zones of the deleted code to indicate a deletion diff (-).
2. Fix bugginess when the user presses "Accept" or "Reject" on a diff. One issue is that when a diff is accepted/rejected all of the diffs below should be updated (because they are now on different line numbers). There are other miscellaneous issues too.
3. Make diff highlighting dynamic. Right now when the user edits text, all of the diffs and their highlights are cleared. Instead, we should update the highlighting of the diff. Each diff lives on a range of lines, and all changes inside that range or intersecting with it should update its highlighting.
## ⭐⭐⭐ Build Cursor-style quick edits (ctrl+k).
When the user presses ctrl+k, an input box should appear inline with the code that they were selecting. This is somewhat difficult to do because an extension alone cannot do this, and it requires creating a new component in the IDE. We think you can modify vscode's built-in "codelens" or "zone widget" components, but we are open to alternatives.
These sometimes get outdated - please refer to our Issues page for the latest issues.
## ⭐⭐⭐ Make History work well.
When the user submits a response or presses the apply/accept/reject button, we should add these events to the history, allowing the user to undo/redo them. Right now there is unexpected behavior if the user tries to undo or redo their changes.
## ⭐⭐⭐ Improve Ctrl+L backend.
## ⭐⭐⭐ Build Cursor-style quick edits (ctrl+k).
Right now, the model outputs entire files. Instead, we should change the prompt so that the model outputs partial changes like `// ... rest of file`. When the user clicks the "Apply" button, the model should rewrite the file and apply the partial changes in the correct locations.
## ⭐⭐ Integrate with Ollama.
We have an Ollama integration coded up in the extension, but it breaks. This is because Ollama has Node.js dependencies like 'path' and 'os' which cannot run in extensions (extensions have to be able to run in the browser). To fix this, we need to migrate Void's extension so that it runs natively into the VS Code editor so that we can access Node.js.
When the user presses ctrl+k, an input box should appear inline with the code that they were selecting. This is somewhat difficult to do because an extension alone cannot do this, and it requires creating a new component in the IDE. We think you can modify vscode's built-in "codelens" or "zone widget" components, but we are open to alternatives.
## ⭐⭐⭐ Creative.
Feel free to build AI features beyond the standard Cursor ones. For example, creating better code search, or supporting AI agents that can edit across files and make multiple LLM calls.
Examples: creating better code search, or supporting AI agents that can edit across files and make multiple LLM calls.
Eventually, we want to build a convenient API for creating AI tools. The API will provide methods for creating the UI (showing an autocomplete suggestion, or creating a new diff), detecting event changes (like `onKeystroke` or `onFileOpen`), and modifying the user's file-system (storing indexes associated with each file), making it much easier to make your own AI plugin. We plan on building these features further along in timeline, but we wanted to list them for completeness.
## ⭐ One-stars.
⭐ When user presses ctrl+L it should clear the sidebar's state.
⭐ Let the user accept / reject all Diffs in an entire file via the sidebar.
⭐ Allow the user to make multiple selections of code or files at once.
⭐ Allow user to X out of their current selection.
# Guidelines
Please don't make big refactors without speaking with us first. We'd like to keep the codebase similar to vscode so we can periodically rebase, and if we have big changes that gets complicated.
@@ -126,38 +127,32 @@ Please don't make big refactors without speaking with us first. We'd like to kee
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.
- 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 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.
- Try to avoid refactoring and making feature changes in the same PR.
# Relevant files
We keep track of all the files we've changed with Void so it's easy to rebase:
- README.md
- CONTRIBUTING.md
- VOID_USEFUL_LINKS.md
- product.json
- package.json
- src/vs/workbench/api/common/{extHost.api.impl.ts | extHostApiCommands.ts}
- src/vs/workbench/workbench.common.main.ts
- src/vs/workbench/contrib/void
- extensions/void
- src/vs/workbench/contrib/void/\*
- extensions/void/\*
- .github/
- .vscode/settings
- .github/\*
- .vscode/settings/\*
- .eslintrc.json
- build/hygiene.js
- build/lib/i18n.resources.json
- build/npm/dirs.js
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
+2 -2
View File
@@ -1,7 +1,7 @@
# Welcome to Void.
Void is the open-source Cursor alternative.
Void is the open-source Cursor alternative.
If you're new, welcome! Feel free to check out our [Project board](https://github.com/orgs/voideditor/projects/2/views/3) for the most pressing Issues to work on, and see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) for instructions on building and running Void.
@@ -19,4 +19,4 @@ For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github
## Support
Feel free to reach out in our [Discord](https://discord.gg/PspNkKG5wt) or contact us via email.
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) or contact us via email.
+7 -7
View File
@@ -9,11 +9,11 @@ const to = 'dist/sidebar/styles.css'
const original_css_contents = fs.readFileSync(from, 'utf8')
postcss([
tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
autoprefixer,
tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
autoprefixer,
])
.process(original_css_contents, { from, to })
.then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
.catch(error => {
console.error('Error in build-css:', error)
})
.process(original_css_contents, { from, to })
.then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
.catch(error => {
console.error('Error in build-css:', error)
})
+8 -8
View File
@@ -2,12 +2,12 @@ const esbuild = require('esbuild')
// Build JS
esbuild.build({
entryPoints: ['src/sidebar/index.tsx'],
bundle: true,
minify: true,
sourcemap: true,
outfile: 'dist/sidebar/index.js',
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
platform: 'browser',
external: ['vscode'],
entryPoints: ['src/sidebar/index.tsx'],
bundle: true,
minify: true,
sourcemap: true,
outfile: 'dist/sidebar/index.js',
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
platform: 'browser',
external: ['vscode'],
}).catch(() => process.exit(1));
+1101 -183
View File
File diff suppressed because it is too large Load Diff
+24 -251
View File
@@ -5,7 +5,7 @@
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "^1.89.0"
"vscode": "^1.92.0"
},
"categories": [
"Other"
@@ -15,243 +15,7 @@
"contributes": {
"configuration": {
"title": "Void",
"properties": {
"void.whichApi": {
"type": "string",
"default": "anthropic",
"description": "Choose an API provider.",
"enum": [
"openAI",
"openAICompatible",
"anthropic",
"azure",
"ollama",
"greptile"
]
},
"void.anthropic.apiKey": {
"type": "string",
"default": "",
"description": "Anthropic API key."
},
"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."
},
"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."
},
"void.greptile.githubPAT": {
"type": "string",
"default": "",
"description": "Github PAT given to Greptile to access your repository."
},
"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": "Your API key."
}
}
"properties": {}
},
"commands": [
{
@@ -260,7 +24,7 @@
},
{
"command": "void.ctrl+k",
"title": "Show Selection Lens"
"title": "Make Inline Edit"
},
{
"command": "void.acceptDiff",
@@ -279,8 +43,9 @@
"command": "void.toggleThreadSelector",
"title": "View past chats",
"icon": "$(history)"
}, {
"command": "void.openSettings",
},
{
"command": "void.toggleSettings",
"title": "Void settings",
"icon": "$(settings-gear)"
}
@@ -328,7 +93,7 @@
"group": "navigation"
},
{
"command": "void.openSettings",
"command": "void.toggleSettings",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
}
@@ -337,46 +102,54 @@
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"build": "rimraf dist && node build-tsx.js && node build-css.js",
"pretest": "tsc -p ./ && eslint src --ext ts",
"test": "vscode-test"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
"@monaco-editor/react": "^4.6.0",
"@rrweb/types": "^2.0.0-alpha.17",
"@types/diff": "^5.2.2",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.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.89.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"@types/vscode": "1.92.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-match-patch": "^1.0.5",
"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",
"lodash": "^4.17.21",
"marked": "^14.1.0",
"ollama": "^0.5.9",
"openai": "^4.68.4",
"postcss": "^8.4.41",
"posthog-js": "^1.176.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.6.1",
"rimraf": "^6.0.1",
"rrweb-snapshot": "^2.0.0-alpha.4",
"tailwindcss": "^3.4.10",
"typescript": "5.5.4",
"typescript-eslint": "^8.3.0"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.27.1",
"diff-match-patch": "^1.0.5",
"ollama": "^0.5.9",
"openai": "^4.57.0",
"diff": "^7.0.0"
"typescript-eslint": "^8.3.0",
"uuid": "^10.0.0"
}
}
+115
View File
@@ -0,0 +1,115 @@
import * as vscode from 'vscode';
import { SuggestedEdit } from './findDiffs';
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 DiffProvider {
originalCodeOfDocument: { [docUri: string]: string }
diffsOfDocument: {
[docUri: string]: {
startLine,
startCol,
endLine,
endCol,
originalText,
inset,
diffid,
}
}
// sweep
currentLine: { [docUri: string]: undefined | number }
weAreEditing: boolean = false
constructor() {
vscode.workspace.onDidChangeTextDocument((e) => {
// on user change, grow/shrink/merge/delete diff AREAS
// you dont have to do anything to the diffs here bc they all get recomputed in refresh()
// user changes only get highlighted if theyre in a diffarea
// go thru all diff areas and adjust line numbers based on the user's change
this.refreshStyles(e.document.uri.toString())
})
}
// refreshes styles on page
refreshStyles(docUriStr: string) {
if (this.weAreEditing) return
// recompute all diffs on the page
// run inset.dispose() on all diffs
// original and current code -> diffs
// originalCodeOfDocument[docUriStr]
// create new diffs
const inset = vscode.window.createWebviewTextEditorInset(editor, lineStart, height, {})
inset.webview.html = `
<html>
<body style="pointer-events:none;">Hello World!</body>
</html>
`;
}
// called on void.acceptDiff
public async acceptDiff({ diffid }: { diffid: number }) {
// update original based on the diff
// refresh()
}
// called on void.rejectDiff
public async rejectDiff({ diffid }: { diffid: number }) {
// get diffs[diffid]
// revert current file based on diff
// refresh()
}
// sweep
initializeSweep({ startLine }) {
// reject all diffs on the page
// store original code
// currentLine=start of sweep
}
onUpdateSweep(addedText) {
// update final
// refresh() ?
// currentLine += number of newlines in addedText
}
onAbortSweep() {
}
}
+19 -19
View File
@@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import { findDiffs } from './findDiffs';
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './shared_types';
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './common/shared_types';
@@ -12,7 +12,7 @@ const greenDecoration = vscode.window.createTextEditorDecorationType({
// responsible for displaying diffs and showing accept/reject buttons
export class DisplayChangesProvider implements vscode.CodeLensProvider {
export class DisplayChangesProvider {
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
@@ -21,22 +21,23 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider {
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)
}
private _onDidChangeDiffsEvent: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
// 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
// update diffs whenever the event fires
this._onDidChangeDiffsEvent.event(() => {
const editor = vscode.window.activeTextEditor
if (!editor) return
let document = editor.document
const docUriStr = document.uri.toString()
return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
})
// this acts as a useEffect. Every time text changes, run this
vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor
@@ -150,13 +151,13 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider {
// 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)
// // print diffs
console.log('!CodeBefore:', JSON.stringify(diffArea.originalCode))
console.log('!CodeAfter:', JSON.stringify(currentCode))
console.log('DiffRepr: ', diffs.map(diff => diff.code).join('\n'))
for (const diff of this._diffsOfDocument[docUriStr]) {
console.log('------------')
console.log('deletedCode:', JSON.stringify(diff.deletedCode))
@@ -165,7 +166,6 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider {
console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,)
}
}
// update green highlighting
@@ -181,7 +181,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider {
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
// update code lenses
this._onDidChangeCodeLenses.fire()
this._onDidChangeDiffsEvent.fire()
}
+9 -29
View File
@@ -2,7 +2,7 @@
import * as vscode from 'vscode';
function getNonce() {
function generateNonce() {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
@@ -19,7 +19,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
private readonly _extensionUri: vscode.Uri
private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration
// private _webviewView?: vscode.WebviewView;
private _webviewDeps: string[] = [];
constructor(context: vscode.ExtensionContext) {
@@ -28,39 +28,18 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
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")
if (!temp_res) throw new Error("Void 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'];
// called by us
updateWebviewHTML(webview: vscode.Webview) {
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 nonce = generateNonce();
const webviewHTML = `<!DOCTYPE html>
<html lang="en">
@@ -68,12 +47,13 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
<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:;">
<meta http-equiv="Content-Security-Policy" content="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>
<div id="ctrlkroot"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
@@ -100,6 +80,6 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
// resolve webview and _webviewView
this._res(webview);
this._webviewView = webviewView;
// this._webviewView = webviewView;
}
}
+261
View File
@@ -0,0 +1,261 @@
import * as vscode from 'vscode';
import { OnFinalMessage, OnText, sendLLMMessage, SetAbort } from "./sendLLMMessage"
import { VoidConfig } from '../sidebar/contextForConfig';
import { findDiffs } from '../findDiffs';
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts';
import { throttle } from 'lodash';
const readFileContentOfUri = async (uri: vscode.Uri) => {
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
.replace(/\r\n/g, '\n') // replace windows \r\n with \n
}
type Res<T> = ((value: T) => void)
const THRTOTLE_TIME = 100 // minimum time between edits
const LINES_PER_CHUNK = 20 // number of lines to search at a time
const applyCtrlLChangesToFile = throttle(
({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr, debug }: { fileUri: vscode.Uri, newCurrentLine: number, oldCurrentLine: number, fullCompletedStr: string, oldFileStr: string, debug?: string }) => {
console.log('DEBUG: ', debug)
console.log('oldNext: ', oldCurrentLine)
console.log('newNext: ', newCurrentLine)
console.log('WRITE_TO_FILE1: ', fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n'))
console.log('WRITE_TO_FILE2: ', oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n'))
// write the change to the file
const WRITE_TO_FILE = (
fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n') // newFile[:newCurrentLine+1]
+ oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n') // oldFile[oldCurrentLine+1:]
)
const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), WRITE_TO_FILE)
vscode.workspace.applyEdit(workspaceEdit)
// highlight the `newCurrentLine` in white
// highlight the remaining part of the file in gray
},
THRTOTLE_TIME, { trailing: true }
)
// `next` is the line after the completed text
// `oldNext` is the same line but in the original file
type CompetedReturn = { isFinished: true, next?: undefined, oldNext?: undefined, } | { isFinished?: undefined, next: number, oldNext: number, }
const generateFileUsingDiffUntilMatchup = ({ fileUri, oldFileStr, completedStr, oldNext, next, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, oldFileStr: string, completedStr: string, oldNext: number, next: number, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => {
const NUM_MATCHUP_TOKENS = 20
const promptContent = `ORIGINAL_FILE
\`\`\`
${oldFileStr}
\`\`\`
DIFF
\`\`\`
${diffStr}
\`\`\`
INSTRUCTIONS
Please finish writing the new file \`NEW_FILE\`. Return ONLY the completion of the file, without any explanation.
NEW_FILE
\`\`\`
${completedStr}
\`\`\`
`
// create a promise that can be awaited
const promise = new Promise<CompetedReturn>((resolve, reject) => {
// get the abort method
let _abort = () => { }
let did_abort = false
sendLLMMessage({
messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }],
onText: (tokenStr, deltaStr) => {
if (did_abort) return;
const fullCompletedStr = completedStr + deltaStr
// diff `originalFileStr` and `newFileStr`
const diffs = findDiffs(oldFileStr, fullCompletedStr)
const lastDiff = diffs[diffs.length - 1]
const oldLineAfterLastDiff = lastDiff.originalEndLine + 1
const newLineAfterLastDiff = lastDiff.endLine + 1
// check if we've generated a diff
const didGenerateDiff = newLineAfterLastDiff > next
// get the line we are currently generating `newCurrentLine`; make sure it never goes past the last diff we've generated
// - if `deltaStr` contains a diff, then _next = newLineAfterLastDiff - 1
// - if it does not contain a diff, then _next = next + deltaStr.split('\n').length - 1
const newCurrentLine = didGenerateDiff ? newLineAfterLastDiff - 1 : next + deltaStr.split('\n').length - 1
const oldCurrentLine = didGenerateDiff ? oldLineAfterLastDiff - 1 : oldNext + (newCurrentLine - next)
// 1. Apply the changes and modify highlighting
applyCtrlLChangesToFile({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr })
// 2. Check for early stopping
// the conditions for early stopping are:
// - we have generated a diff
// - there is matchup with the original file after the diff
const isMatchupAfterDiff = fullCompletedStr.split('\n').slice(newLineAfterLastDiff).join('\n').length > NUM_MATCHUP_TOKENS
if (didGenerateDiff && isMatchupAfterDiff) {
// resolve the promise
resolve({ next: newCurrentLine + 1, oldNext: oldCurrentLine + 1, });
// abort the LLM call
_abort()
did_abort = true
} else {
}
},
onFinalMessage: (deltaStr) => {
const newCompletedStr = completedStr + deltaStr
applyCtrlLChangesToFile({ fileUri, newCurrentLine: Number.MAX_SAFE_INTEGER, oldCurrentLine: Number.MAX_SAFE_INTEGER, fullCompletedStr: newCompletedStr, oldFileStr, debug: 'FINAL' })
resolve({ isFinished: true });
},
onError: (e) => {
resolve({ isFinished: true });
console.error('Error rewriting file with diff', e);
},
voidConfig,
setAbort: (a) => { setAbort(a); _abort = a; },
})
})
return promise
}
const shouldApplyDiffFn = ({ diffStr, fileStr, speculationStr, voidConfig, setAbort }: { diffStr: string, fileStr: string, speculationStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => {
const promptContent = `DIFF
\`\`\`
${diffStr}
\`\`\`
FILES
\`\`\`
${fileStr}
\`\`\`
SELECTION
\`\`\`
${speculationStr}
\`\`\`
Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it should not be modified. You should respond only with \`true\` or \`false\` and nothing else.
`
// create new promise
const promise = new Promise<boolean>((resolve, reject) => {
// send message to LLM
sendLLMMessage({
messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }],
onFinalMessage: (finalMessage) => {
const containsTrue = finalMessage
.slice(-10) // check for `true` in last 10 characters
.toLowerCase()
.includes('true')
resolve(containsTrue)
},
onError: (e) => {
resolve(false);
console.error('Error in shouldApplyDiff: ', e)
},
onText: () => { },
voidConfig,
setAbort,
})
})
// return the promise
return promise
}
// lazily applies the diff to the file
// we chunk the text in the file, and ask an LLM whether it should edit each chunk
const applyDiffLazily = async ({ fileUri, oldFileStr, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, oldFileStr: string, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => {
// stateful variables
let next = 0
let oldNext = 0
while (next < oldFileStr.split('\n').length) {
console.log('next line: ', next)
// get the chunk
const chunkStr = oldFileStr.split('\n').slice(next, next + LINES_PER_CHUNK).join('\n')
// ask LLM if we should apply the diff to the chunk
const __start = new Date().getTime()
let shouldApplyDiff = await shouldApplyDiffFn({ fileStr: oldFileStr, speculationStr: chunkStr, diffStr, voidConfig, setAbort })
const __end = new Date().getTime()
if (!shouldApplyDiff) { // should not change the chunk
console.log('KEEP CHUNK time: ', __end - __start)
next += LINES_PER_CHUNK
oldNext += LINES_PER_CHUNK
continue;
}
// ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting)
// make vscode read uri = 'asdasd'
const ___start = new Date().getTime()
const completedStr = (await readFileContentOfUri(fileUri)).split('\n').slice(0, next).join('\n');
const result = await generateFileUsingDiffUntilMatchup({ fileUri, oldFileStr, completedStr, oldNext, next, diffStr, voidConfig, setAbort, })
const ___end = new Date().getTime()
console.log('EDIT CHUNK time: ', ___end - ___start);
// if we are finished, stop the loop
if (result.isFinished) {
break;
}
next = result.next
oldNext = result.oldNext
}
}
export { applyDiffLazily }
+128 -115
View File
@@ -1,81 +1,69 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { VoidConfig } from '../sidebar/contextForConfig';
// 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,
model: string,
},
greptile: {
apikey: string,
githubPAT: string,
repoinfo: {
remote: string, // e.g. 'github'
repository: string, // e.g. 'voideditor/void'
branch: string // e.g. 'main'
}
},
ollama: {
endpoint: string,
model: string
},
openAICompatible: {
endpoint: string,
model: string,
apikey: string
}
whichApi: string
export type OnText = (newText: string, fullText: string) => void
export type OnFinalMessage = (input: string) => void
export type SetAbort = (abort: () => void) => void
export type LLMMessageAnthropic = {
role: 'user' | 'assistant',
content: string,
}
type OnText = (newText: string, fullText: string) => void
export type LLMMessage = {
role: 'user' | 'assistant',
content: string
role: 'system' | 'user' | 'assistant',
content: string,
}
type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: (input: string) => void,
apiConfig: ApiConfig,
})
=> {
abort: () => void
}
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
voidConfig: VoidConfig,
setAbort: SetAbort,
}) => void
type SendLLMMessageFnTypeExternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: (input: string) => void,
apiConfig: ApiConfig | null,
onError: (error: string) => void,
voidConfig: VoidConfig | null,
setAbort: SetAbort,
})
=> {
abort: () => void
}
=> void
// Claude
const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
// Anthropic
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
const anthropic = new Anthropic({ apiKey: apiConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
// find system messages and concatenate them
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// remove system messages for Anthropic
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
const stream = anthropic.messages.stream({
model: apiConfig.anthropic.model,
max_tokens: parseInt(apiConfig.anthropic.maxTokens),
messages: messages,
system: systemMessage,
messages: anthropicMessages,
model: voidConfig.anthropic.model,
max_tokens: parseInt(voidConfig.default.maxTokens),
});
let did_abort = false
@@ -94,22 +82,30 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
onFinalMessage(content)
})
stream.on('error', (error) => {
// the most common error will be invalid API key (401), so we handle this with a nice message
if (error instanceof Anthropic.APIError && error.status === 401) {
onError('Invalid API key.')
}
else {
onError(error.message)
}
})
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
const abort = () => {
// stream.abort() // this doesnt appear to do anything, but it should try to stop claude from generating anymore
did_abort = true
stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
}
return { abort }
setAbort(abort)
};
// OpenAI and OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
// OpenAI, OpenRouter, OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
let didAbort = false
let fullText = ''
@@ -122,17 +118,27 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
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, }
if (voidConfig.default.whichApi === 'openAI') {
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
}
else if (apiConfig.whichApi === 'openAICompatible') {
openai = new OpenAI({ apiKey: apiConfig.openAI.apikey, dangerouslyAllowBrowser: true });
options = { model: apiConfig.openAICompatible.model, messages: messages, stream: true, }
else if (voidConfig.default.whichApi === 'openRouter') {
openai = new OpenAI({
baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
defaultHeaders: {
"HTTP-Referer": 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
"X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
}
else if (voidConfig.default.whichApi === 'openAICompatible') {
openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
}
else {
console.error(`sendOpenAIMsg: invalid whichApi: ${apiConfig.whichApi}`)
throw new Error(`apiConfig.whichAPI was invalid: ${apiConfig.whichApi}`)
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
}
openai.chat.completions
@@ -143,27 +149,35 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
didAbort = true;
}
// when receive text
try {
for await (const chunk of response) {
if (didAbort) return;
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
for await (const chunk of response) {
if (didAbort) return;
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText(newText, fullText);
}
// when error/fail
catch (error) {
console.error('Error in OpenAI stream:', error);
onFinalMessage(fullText);
onFinalMessage(fullText);
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {
if (error instanceof OpenAI.APIError) {
if (error.status === 401) {
onError('Invalid API key.');
}
else {
onError(error.message);
}
}
else {
onError(error);
}
})
return { abort };
setAbort(abort)
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
let didAbort = false
let fullText = ""
@@ -173,12 +187,13 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
didAbort = true;
};
const ollama = new Ollama({ host: apiConfig.ollama.endpoint })
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
ollama.chat({
model: apiConfig.ollama.model,
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseInt(voidConfig.default.maxTokens) } // this is max_tokens
})
.then(async stream => {
abort = () => {
@@ -186,22 +201,21 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
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);
for await (const chunk of stream) {
if (didAbort) return;
const newText = chunk.message.content;
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
return { abort };
// when error/fail
.catch(error => {
onError(error)
})
setAbort(abort);
};
@@ -210,7 +224,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
let didAbort = false
let fullText = ''
@@ -222,14 +236,14 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
"Authorization": `Bearer ${apiConfig.greptile.apikey}`,
"X-Github-Token": `${apiConfig.greptile.githubPAT}`,
"Authorization": `Bearer ${voidConfig.greptile.apikey}`,
"X-Github-Token": `${voidConfig.greptile.githubPAT}`,
"Content-Type": `application/json`,
},
body: JSON.stringify({
messages,
stream: true,
repositories: [apiConfig.greptile.repoinfo]
repositories: [voidConfig.greptile.repoinfo],
}),
})
// this is {message}\n{message}\n{message}...\n
@@ -268,33 +282,32 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
})
.catch(e => {
console.error('Error in Greptile stream:', e);
onFinalMessage(fullText);
onError(e)
});
return { abort }
setAbort(abort)
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
if (!apiConfig) return { abort: () => { } }
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
if (!voidConfig) return;
switch (apiConfig.whichApi) {
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
switch (voidConfig.default.whichApi) {
case 'anthropic':
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig });
return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort });
case 'openAI':
case 'openRouter':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig });
case 'greptile':
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig });
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort });
case 'ollama':
return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig });
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort });
case 'greptile':
return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort });
default:
console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`);
return { abort: () => { } }
//return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
}
}
@@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import { ApiConfig } from './common/sendLLMMessage';
import { PartialVoidConfig } from '../sidebar/contextForConfig';
@@ -26,9 +26,9 @@ 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
deletedRange: vscode.Range; // relative to the original file, inclusive
insertedRange: vscode.Range;
deletedCode: string;
deletedCode: string; // relative to the new file, inclusive
insertedCode: string;
}
@@ -38,50 +38,36 @@ type Diff = {
lenses: vscode.CodeLens[],
} & BaseDiff
type WebviewMessage = (
// editor -> sidebar
// editor -> sidebar
type MessageToSidebar = (
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
// sidebar -> editor
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
// sidebar -> editor
| { type: 'requestFiles', filepaths: vscode.Uri[] }
// editor -> sidebar
| { type: 'ctrl+k', selection: CodeSelection }
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
// sidebar -> editor
| { type: 'getApiConfig' }
// editor -> sidebar
| { type: 'apiConfig', apiConfig: ApiConfig }
// sidebar -> editor
| { type: 'getAllThreads' }
// editor -> sidebar
| { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
| { type: 'allThreads', threads: ChatThreads }
// sidebar -> editor
| { type: 'persistThread', thread: ChatThreads[string] }
// editor -> sidebar
| { type: 'startNewThread' }
// editor -> sidebar
| { type: 'toggleThreadSelector' }
| { type: 'toggleSettings' }
| { type: 'deviceId', deviceId: string }
)
// sidebar -> editor
type MessageFromSidebar = (
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
| { type: 'requestFiles', filepaths: vscode.Uri[] }
| { type: 'getPartialVoidConfig' }
| { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig }
| { type: 'getAllThreads' }
| { type: 'persistThread', thread: ChatThreads[string] }
| { type: 'getDeviceId' }
)
type Command = WebviewMessage['type']
type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string;
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
}
}
@@ -97,7 +83,12 @@ type ChatMessage =
| {
role: "assistant";
content: string; // content received from LLM
displayContent: string; // content displayed to user (this is the same as content for now)
displayContent: string | undefined; // content displayed to user (this is the same as content for now)
}
| {
role: "system";
content: string;
displayContent?: undefined;
}
export {
@@ -105,8 +96,8 @@ export {
Diff, DiffArea,
CodeSelection,
File,
WebviewMessage,
Command,
MessageFromSidebar,
MessageToSidebar,
ChatThreads,
ChatMessage,
}
+406
View File
@@ -0,0 +1,406 @@
const generateDiffInstructions = `
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
All changes made to files must be outputted in unified diff format.
Unified diff format instructions:
1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
3. Make diffs more than a few lines.
4. Make high-level diffs rather than many one-line diffs.
Here's an example of unified diff format:
\`\`\`
@@ ... @@
-def factorial(n):
- if n == 0:
- return 1
- else:
- return n * factorial(n-1)
+def factorial(number):
+ if number == 0:
+ return 1
+ else:
+ return number * factorial(number-1)
\`\`\`
Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
\`\`\`
@@ ... @@ # This is less preferred because edits are close together and should be grouped:
-def factorial(n):
+def factorial(number):
- if n == 0:
+ if number == 0:
return 1
else:
- return n * factorial(n-1)
+ return number * factorial(number-1)
\`\`\`
# Example 1:
FILES
selected file \`test.ts\`:
\`\`\`
x = 1
{{selection}}
z = 3
\`\`\`
SELECTION
\`\`\`const y = 2\`\`\`
INSTRUCTIONS
\`\`\`y = 3\`\`\`
EXPECTED RESULT
We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
\`\`\`
@@ ... @@
-x = 1
-
-y = 2
+x = 1
+
+y = 3
\`\`\`
# Example 2:
FILES
selected file \`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
{{selection}}
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
SELECTION
\`\`\` <button\`\`\`
INSTRUCTIONS
\`\`\`make all the buttons like this into divs\`\`\`
EXPECTED OUTPUT
We should change all the buttons like the one selected into a div component. Here is the change:
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
`;
const searchDiffChunkInstructions = `
You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
# Example 1:
FILES
selected file \`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
SELECTION
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
\`\`\`
RESULT
The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
OUTPUT
\`true\`
`
const writeFileWithDiffInstructions = `
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
Please finish writing the new file \`new_file\`, according to the diff \`diff\`.
Directions:
1. Continue exactly where the new file \`new_file\` left off.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
# Example 1:
ORIGINAL_FILE
\`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
NEW_FILE
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
\`\`\`
COMPLETION
\`\`\`
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<div
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</div>
</li>
))}
</ul>
<div className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</div>
</div>
);
};
export default Sidebar;\`\`\`
`
export {
generateDiffInstructions,
searchDiffChunkInstructions,
writeFileWithDiffInstructions,
};
+111 -89
View File
@@ -1,87 +1,97 @@
import * as vscode from 'vscode';
import { DisplayChangesProvider } from './DisplayChangesProvider';
import { BaseDiffArea, ChatThreads, WebviewMessage } from './shared_types';
import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider';
import { ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types';
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
import { ApiConfig } from './common/sendLLMMessage';
import { v4 as uuidv4 } from 'uuid'
import { applyDiffLazily } from './common/ctrlL';
import { getVoidConfig } from './sidebar/contextForConfig';
import { DiffProvider } from './DiffProvider';
// import { getVoidConfig } from './sidebar/contextForConfig';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
const readFileContentOfUri = async (uri: vscode.Uri) => {
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.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.greptile').get('apiKey') ?? '',
githubPAT: vscode.workspace.getConfiguration('void.greptile').get('githubPAT') ?? '',
repoinfo: {
remote: 'github',
repository: 'TODO',
branch: 'main'
}
},
ollama: {
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
const roundRangeToLines = (selection: vscode.Selection) => {
return new vscode.Range(selection.start.line, 0, selection.end.line, Number.MAX_SAFE_INTEGER)
}
export function activate(context: vscode.ExtensionContext) {
// 1. Mount the chat sidebar
const webviewProvider = new SidebarWebviewProvider(context);
const sidebarWebviewProvider = new SidebarWebviewProvider(context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, webviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
);
// 2. Activate the sidebar on ctrl+l
// 2. ctrl+l
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+l', () => {
const editor = vscode.window.activeTextEditor
if (!editor)
return
if (!editor) return
// show the sidebar
vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
// vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
// get the text the user is selecting
const selectionStr = editor.document.getText(editor.selection);
// get the range of the selection
const selectionRange = editor.selection;
const selectionRange = roundRangeToLines(editor.selection);
// get the text the user is selecting
const selectionStr = editor.document.getText(selectionRange);
// get the file the user is in
const filePath = editor.document.uri;
// send message to the webview (Sidebar.tsx)
webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies WebviewMessage));
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
})
);
// 3. Show an approve/reject codelens above each change
const displayChangesProvider = new DisplayChangesProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider));
// 2.5: ctrl+k
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+k', () => {
console.log('CTRLK PRESSED')
const editor = vscode.window.activeTextEditor
if (!editor) return
// get the range of the selection
const selectionRange = roundRangeToLines(editor.selection);
// get the text the user is selecting
const selectionStr = editor.document.getText(selectionRange);
// get the file the user is in
const filePath = editor.document.uri;
// send message to the webview (Sidebar.tsx)
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
})
);
// 3. Show an approve/reject
const displayChangesProvider = new DiffProvider();
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider));
// 4. Add approve/reject commands
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
@@ -91,33 +101,23 @@ export function activate(context: vscode.ExtensionContext) {
displayChangesProvider.rejectDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.openSettings', async () => {
vscode.commands.executeCommand('workbench.action.openSettings', '@ext:void.void');
}));
// 5. Receive messages from sidebar
webviewProvider.webview.then(
sidebarWebviewProvider.webview.then(
webview => {
// top navigation bar commands
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage)
webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
webview.postMessage({ type: 'toggleThreadSelector' } satisfies WebviewMessage)
webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
}))
// when config changes, send it to the sidebar
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('void')) {
const apiConfig = getApiConfig()
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
}
})
context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => {
webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar)
}));
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
webview.onDidReceiveMessage(async (m: WebviewMessage) => {
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
if (m.type === 'requestFiles') {
@@ -127,9 +127,10 @@ export function activate(context: vscode.ExtensionContext) {
)
// send contents to webview
webview.postMessage({ type: 'files', files, } satisfies WebviewMessage)
webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
} else if (m.type === 'applyChanges') {
}
else if (m.type === 'applyChanges') {
const editor = vscode.window.activeTextEditor
if (!editor) {
@@ -137,15 +138,15 @@ export function activate(context: vscode.ExtensionContext) {
return
}
// 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)
// // create an area to show diffs
// const diffArea = {
// 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
@@ -154,29 +155,51 @@ export function activate(context: vscode.ExtensionContext) {
// 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);
});
const fileUri = editor.document.uri
const fileStr = await readFileContentOfUri(fileUri)
const voidConfig = getVoidConfig(context.globalState.get('partialVoidConfig') ?? {})
let abort = () => { } // TODO this is unused
// apply the change
applyDiffLazily({ fileUri, oldFileStr: fileStr, diffStr: m.code, voidConfig, setAbort: (a) => { abort = a } })
// set the file equal to the change
// 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)
displayChangesProvider.refreshLenses(editor, editor.document.uri.toString())
}
else if (m.type === 'getApiConfig') {
const apiConfig = getApiConfig()
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
else if (m.type === 'getPartialVoidConfig') {
const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar)
}
else if (m.type === 'persistPartialVoidConfig') {
const partialVoidConfig = m.partialVoidConfig
context.globalState.update('partialVoidConfig', partialVoidConfig)
}
else if (m.type === 'getAllThreads') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
webview.postMessage({ type: 'allThreads', threads } satisfies WebviewMessage)
webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
}
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 if (m.type === 'getDeviceId') {
let deviceId = context.globalState.get('void_deviceid')
if (!deviceId || typeof deviceId !== 'string') {
deviceId = uuidv4()
context.globalState.update('void_deviceid', deviceId)
}
webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar)
}
else {
console.error('unrecognized command', m.type, m)
console.error('unrecognized command', m)
}
})
}
@@ -200,4 +223,3 @@ export function activate(context: vscode.ExtensionContext) {
// )
}
+248 -224
View File
@@ -1,178 +1,213 @@
import * as vscode from 'vscode';
// import { diffLines, Change } from 'diff';
import { BaseDiff } from './shared_types';
import { diffLines, Change } from 'diff';
import { diff_match_patch } from 'diff-match-patch';
export type SuggestedEdit = {
// start/end of current file
startLine: number;
startCol: number;
endLine: number;
endCol: number;
// start/end of original file
originalStartLine: number,
originalStartCol: number,
originalEndLine: number,
originalEndCol: number,
type: 'insertion' | 'deletion' | 'edit',
originalContent: string, // original content (originalfile[originalStart...originalEnd])
newContent: string,
}
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;
export function findDiffs(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);
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) {
// do nothing
// if we were on a streak of +s and -s, end it
if (streakStartInNewFile !== undefined) {
let type: 'edit' | 'insertion' | 'deletion' = 'edit'
let startLine = streakStartInNewFile
let endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
let startCol = 0
let endCol = Number.MAX_SAFE_INTEGER
let originalStartLine = streakStartInOldFile!
let originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
let originalStartCol = 0
let originalEndCol = Number.MAX_SAFE_INTEGER
let newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
let originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
// if the range is empty, mark it as a deletion / insertion (both won't be true at once)
// DELETION
if (endLine === startLine - 1) {
type = 'deletion'
endLine = startLine
startCol = 0
endCol = 0
newContent += '\n'
}
// INSERTION
else if (originalEndLine === originalStartLine - 1) {
type = 'insertion'
originalEndLine = originalStartLine
originalStartCol = 0
originalEndCol = 0
}
const replacement: SuggestedEdit = {
type,
startLine, startCol, endLine, endCol, newContent,
originalStartLine, originalStartCol, originalEndLine, originalEndCol, originalContent
} as SuggestedEdit
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
}
// 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;
};
// const diffLinesOld = (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 findDiffsOld = (oldText: string, newText: string): BaseDiff[] => {
// export const findDiffs = (oldText: string, newText: string): DiffBlock[] => {
// const diffs = diffLines(oldText, newText);
// const blocks: DiffBlock[] = [];
// const diffs = diffLinesOld(oldText, newText);
// const blocks: BaseDiff[] = [];
// let reprBlock: string[] = [];
// let deletedBlock: string[] = [];
// let insertedBlock: string[] = [];
// let insertedEnd = 0;
// let deletedEnd = 0;
// let insertedLine = 0;
// let deletedLine = 0;
// let insertedStart = 0;
// let deletedStart = 0;
// diffs.forEach(part => {
// diffs.forEach(([operation, text]) => {
// part.count = part.count ?? 0
// const lines = text.split('\n');
// // 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'));
// }
// 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;
// }
// });
// // 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
// // Add any remaining blocks after the loop ends
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
// blocks.push({
// code: reprBlock.join('\n'),
// code: reprBlock.join(''),
// 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),
// deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
// });
// }
// return blocks;
// }
// };
@@ -181,97 +216,86 @@ export const findDiffs = (oldText: string, newText: string): BaseDiff[] => {
// export const findDiffs = (oldText: string, newText: string): BaseDiff[] => {
// let diffs = diffLines(oldText, newText)
// .map(diff => {
// const operation = diff.added ? 1 : diff.removed ? -1 : 0;
// const text = diff.value;
// return [operation, text] as const;
// })
// const blocks: BaseDiff[] = [];
// let reprBlock: string[] = [];
// let deletedBlock: string[] = [];
// let insertedBlock: string[] = [];
// let newFileLine = 0;
// let oldFileLine = 0;
// let insertedStart = 0;
// let deletedStart = 0;
// import { diffLines, Change } from 'diff';
// diffs.forEach(([operation, text]) => {
// export type SuggestedEdit = {
// // start/end of current file
// startLine: number;
// endLine: number;
// const lines = text.split('\n');
// // start/end of original file
// originalStartLine: number,
// originalEndLine: number,
// switch (operation) {
// // original content (originalfile[originalStart...originalEnd])
// originalContent: string;
// newContent: string;
// }
// // insertion
// case 1:
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
// if (insertedBlock.length === 0) insertedStart = newFileLine;
// newFileLine += lines.length - 1; // update the line count for new text
// insertedBlock.push(text);
// reprBlock.push(lines.map(line => `+ ${line}`).join('\n'));
// break;
// 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)
// // deletion
// case -1:
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
// if (deletedBlock.length === 0) deletedStart = oldFileLine;
// oldFileLine += lines.length - 1; // update the line count for old text
// deletedBlock.push(text);
// reprBlock.push(lines.map(line => `- ${line}`).join('\n'));
// break;
// lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
// // no change
// case 0:
// // add pending block 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, oldFileLine, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, newFileLine, Number.MAX_SAFE_INTEGER),
// });
// }
// let oldFileLineNum: number = 0;
// let newFileLineNum: number = 0;
// // update variables
// reprBlock = [];
// deletedBlock = [];
// insertedBlock = [];
// deletedStart += lines.length - 1;
// insertedStart += lines.length - 1;
// newFileLine += lines.length - 1;
// oldFileLine += lines.length - 1;
// 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;
// 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, oldFileLine, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, newFileLine, Number.MAX_SAFE_INTEGER),
// });
// }
// // line was removed from old file
// else if (line.removed) {
// return blocks;
// };
// // 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
// }
+20
View File
@@ -0,0 +1,20 @@
import React, { useState } from 'react';
import { useOnVSCodeMessage } from './getVscodeApi';
export const CtrlK = () => {
const [x, sx] = useState('abc')
useOnVSCodeMessage('ctrl+k', () => {
console.log('Ctrl+K pressed')
sx('Pressed ctrl+k')
})
return <>
<div>
{x}
</div>
</>
};
+45 -299
View File
@@ -1,319 +1,65 @@
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 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[]) => {
return fullFiles.map(({ filepath, content }) =>
`
${filepath.fsPath}
\`\`\`
${content}
\`\`\``).join('\n')
}
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
return `
${filesStr(files)}
${!selection ? '' : `
I am currently selecting this code:
\`\`\`${selection.selectionStr}\`\`\`
`}
Please edit the code following these instructions (or, if appropriate, answer my question instead):
${instructions}
If you make a change, rewrite the entire file.
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
}
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
const role = chatMessage.role
const children = chatMessage.displayContent
if (!children)
return null
let chatbubbleContents: React.ReactNode
if (role === 'user') {
chatbubbleContents = <>
<SelectedFiles files={chatMessage.files} setFiles={null} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
{children}
</>
}
else if (role === 'assistant') {
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
{chatbubbleContents}
</div>
</div>
}
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>
)
}
import { CodeSelection, ChatMessage, MessageToSidebar } from "../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi"
import { SidebarThreadSelector } from "./SidebarThreadSelector";
import { SidebarChat } from "./SidebarChat";
import { SidebarSettings } from './SidebarSettings';
import { identifyUser } from "./metrics/posthog";
const Sidebar = () => {
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
// state of current message
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
const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
// state of chat
const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat')
const abortFnRef = useRef<(() => void) | null>(null)
// if they pressed the + to add a new chat
useOnVSCodeMessage('startNewThread', (m) => {
setTab('chat');
chatInputRef.current?.focus();
})
const [apiConfig, setApiConfig] = useState<ApiConfig | null>(null)
// ctrl+l should switch back to chat
useOnVSCodeMessage('ctrl+l', (m) => {
setTab('chat');
chatInputRef.current?.focus();
})
// get Api Config on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: 'getApiConfig' })
}, [])
// if they toggled thread selector
useOnVSCodeMessage('toggleThreadSelector', (m) => {
if (tab === 'threadSelector') {
setTab('chat')
chatInputRef.current?.blur();
} else
setTab('threadSelector')
})
// Receive messages from the extension
useEffect(() => {
const listener = (event: MessageEvent) => {
const m = event.data as WebviewMessage;
// resolve any awaiting promises
// eg. it will resolve the promise below for `await VSCodeResponse('files')`
resolveAwaitingVSCodeResponse(m)
// 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 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
else if (m.type === 'apiConfig') {
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, startNewThread, currentThread])
const formRef = useRef<HTMLFormElement | null>(null)
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (isLoading) return
setIsLoading(true)
setInstructions('');
formRef.current?.reset(); // reset the form's text
setSelection(null)
setFiles([])
// request file content from vscode and await response
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
const relevantFiles = await awaitVSCodeResponse('files')
// add message to chat history
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
// console.log('prompt:\n', content)
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
addMessageToHistory(newHistoryElt)
// send message to LLM
let { abort } = sendLLMMessage({
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, and clear selection
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
addMessageToHistory(newHistoryElt)
// clear selection
setMessageStream('')
setIsLoading(false)
},
apiConfig: apiConfig
})
abortFnRef.current = abort
}
const onStop = useCallback(() => {
// abort claude
abortFnRef.current?.()
// if messageStream was not empty, add it to the history
const llmContent = messageStream || '(canceled)'
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
}, [addMessageToHistory, messageStream])
//Clear code selection
const clearSelection = () => {
setSelection(null);
};
// if they toggled settings
useOnVSCodeMessage('toggleSettings', (m) => {
if (tab === 'settings') {
setTab('chat')
chatInputRef.current?.blur();
} else
setTab('settings')
})
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 */}
{currentThread !== null && currentThread.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
<div className={`flex flex-col h-screen w-full`}>
<div className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
<SidebarThreadSelector onClose={() => setTab('chat')} />
</div>
{/* chatbar */}
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
<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>
<div className={`${tab !== 'chat' && tab !== 'threadSelector' ? 'hidden' : ''}`}>
<SidebarChat chatInputRef={chatInputRef} />
</div>
<div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
<SidebarSettings />
</div>
</div>
</>
}
+384
View File
@@ -0,0 +1,384 @@
import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react";
import { marked } from 'marked';
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import { File, ChatMessage, CodeSelection } from "../common/shared_types";
import * as vscode from 'vscode'
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi";
import { useThreads } from "./contextForThreads";
import { sendLLMMessage } from "../common/sendLLMMessage";
import { useVoidConfig } from "./contextForConfig";
import { generateDiffInstructions } from "../common/systemPrompts";
import { captureEvent } from "./metrics/posthog";
const filesStr = (fullFiles: File[]) => {
return fullFiles.map(({ filepath, content }) =>
`
${filepath.fsPath}
\`\`\`
${content}
\`\`\``).join('\n')
}
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
let str = '';
if (files.length > 0) {
str += filesStr(files);
}
if (selection) {
str += `
I am currently selecting this code:
\t\`\`\`${selection.selectionStr}\`\`\`
`;
}
if (files.length > 0 && selection) {
str += `
Please edit the selected code or the entire file following these instructions:
`;
} else if (files.length > 0) {
str += `
Please edit the file following these instructions:
`;
} else if (selection) {
str += `
Please edit the selected code following these instructions:
`;
}
str += `
\t${instructions}
`;
if (files.length > 0) {
str += `
\tIf you make a change, rewrite the entire file.
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
}
return str;
};
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>
)
)
}
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
const role = chatMessage.role
const children = chatMessage.displayContent
if (!children)
return null
let chatbubbleContents: React.ReactNode
if (role === 'user') {
chatbubbleContents = <>
<SelectedFiles files={chatMessage.files} setFiles={null} />
{chatMessage.selection?.selectionStr && <BlockCode
text={chatMessage.selection.selectionStr}
buttonsOnHover={null}
/>}
{children}
</>
}
else if (role === 'assistant') {
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
{chatbubbleContents}
</div>
</div>
}
export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HTMLTextAreaElement> }) => {
// state of current message
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 [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null)
const [latestError, setLatestError] = useState('')
// higher level state
const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
const { voidConfig } = useVoidConfig()
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = useCallback((eventId: string, extras?: object) => {
const whichApi = voidConfig.default['whichApi']
const messages = getCurrentThread()?.messages
captureEvent(eventId, {
whichApi: whichApi,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent?.length })),
version: '2024-10-19',
...extras,
})
}, [getCurrentThread, voidConfig.default])
// if they pressed the + to add a new chat
useOnVSCodeMessage('startNewThread', (m) => {
const allThreads = getAllThreads()
// find a thread with 0 messages and switch to it
for (let threadId in allThreads) {
if (allThreads[threadId].messages.length === 0) {
switchToThread(threadId)
return
}
}
// start a new thread
startNewThread()
})
// if user pressed ctrl+l, add their selection to the sidebar
useOnVSCodeMessage('ctrl+l', (m) => {
setSelection(m.selection)
const filepath = m.selection.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])
})
const isDisabled = !instructions
const formRef = useRef<HTMLFormElement | null>(null)
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (isDisabled) return
if (isLoading) return
setIsLoading(true)
setInstructions('');
formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
setSelection(null)
setFiles([])
setLatestError('')
// request file content from vscode and await response
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
const relevantFiles = await awaitVSCodeResponse('files')
// add system message to chat history
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
addMessageToHistory(systemPromptElt)
const userContent = userInstructionsStr(instructions, relevantFiles.files, selection)
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
addMessageToHistory(newHistoryElt)
captureChatEvent('Chat - Sending Message', { messageLength: instructions.length })
const submit_time = new Date()
// send message to LLM
sendLLMMessage({
messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
onText: (newText, fullText) => setMessageStream(fullText),
onFinalMessage: (content) => {
captureChatEvent('Chat - Received Full Message', { messageLength: content.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
// add assistant's message to chat history, and clear selection
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
},
onError: (error) => {
captureChatEvent('Chat - Error', { error })
// add assistant's message to chat history, and clear selection
let content = messageStream; // just use the current content
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
setLatestError(error)
},
setAbort: (abort) => {
abortFnRef.current = abort
},
voidConfig,
})
}
const onAbort = useCallback(() => {
captureChatEvent('Chat - Abort', { messageLengthSoFar: messageStream.length })
// abort claude
abortFnRef.current?.()
// if messageStream was not empty, add it to the history
const llmContent = messageStream || '(null)'
const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
}, [captureChatEvent, messageStream, addMessageToHistory])
return <>
<div className="overflow-x-hidden space-y-4">
{/* previous messages */}
{getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
</div>
{/* chatbar */}
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
<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 text={selection.selectionStr}
buttonsOnHover={(
<button
onClick={() => setSelection(null)}
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!')
onSubmit(e)
}}>
{/* input */}
<textarea
ref={chatInputRef}
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
/>
{isLoading ?
// stop button
<button
onClick={onAbort}
type='button'
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
>
<svg
className='scale-50'
stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M24 24H0V0h24v24z"></path>
</svg>
</button>
:
// submit button (up arrow)
<button
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={isDisabled}
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>
{/* error message */}
{!latestError ? null : <div>
{latestError}
</div>}
</div>
</>
}
@@ -0,0 +1,105 @@
import React, { useState } from "react";
import { configFields, useVoidConfig, VoidConfigField } from "./contextForConfig";
const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => {
const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig()
const { enumArr, defaultVal, description } = voidConfigInfo[field][param]
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
const updateState = (newValue: string) => { setConfigParam(field, param, newValue) }
const resetButton = <button
disabled={val === defaultVal}
title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
onClick={() => updateState(defaultVal)}
>
<svg
className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
fill="currentColor" strokeWidth="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M3.5 2v3.5L4 6h3.5V5H4.979l.941-.941a3.552 3.552 0 1 1 5.023 5.023L5.746 14.28l.72.72 5.198-5.198A4.57 4.57 0 0 0 5.2 3.339l-.7.7V2h-1z"></path>
</svg>
</button>
const inputElement = enumArr === undefined ?
// string
(<input
className='input p-1 w-full'
type="text"
value={val}
onChange={(e) => updateState(e.target.value)}
/>)
:
// enum
(<select
className='dropdown p-1 w-full'
value={val}
onChange={(e) => updateState(e.target.value)}
>
{enumArr.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>)
return <div>
<label className='hidden'>{param}</label>
<span>{description}</span>
<div className='flex items-center'>
{inputElement}
{resetButton}
</div>
</div>
}
export const SidebarSettings = () => {
const { voidConfig, voidConfigInfo } = useVoidConfig()
const current_field = voidConfig.default['whichApi'] as VoidConfigField
return (
<div className='space-y-4 py-2 overflow-y-auto'>
{/* choose the field */}
<div className='outline-vscode-input-bg'>
<SettingOfFieldAndParam
field='default'
param='whichApi'
/>
<SettingOfFieldAndParam
field='default'
param='maxTokens'
/>
</div>
<hr />
{/* render all fields, but hide the ones not visible for fast tab switching */}
{configFields.map(field => {
return <div
key={field}
className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
>
{Object.keys(voidConfigInfo[field]).map((param) => (
<SettingOfFieldAndParam
key={param}
field={field}
param={param}
/>
))}
</div>
})}
{/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */}
<div className='pt-20'>
{`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `}
{`type 'Open User Settings (JSON)',`}
{` and look for 'void.'. `}
</div>
</div>
)
}
@@ -0,0 +1,79 @@
import React from "react";
import { ThreadsProvider, useThreads } from "./contextForThreads";
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
const { getAllThreads, getCurrentThread, switchToThread } = useThreads()
const allThreads = getAllThreads()
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
return (
<div className="flex flex-col gap-y-1">
{/* X button at top right */}
<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>
{/* a list of all the past threads */}
<div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
{sortedThreadIds.map((threadId) => {
if (!allThreads)
return <>Error: Threads not found.</>
const pastThread = allThreads[threadId]
let btnStringArr = []
let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
btnStringArr.push(msg1)
let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
if (msg2)
btnStringArr.push(msg2)
btnStringArr.push(allThreads[threadId].messages.length)
const btnString = btnStringArr.join(' / ')
return (
<button
key={pastThread.id}
className={`btn btn-sm rounded-sm ${pastThread.id === getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
onClick={() => switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
{btnString}
</button>
)
})}
</div>
</div>
)
}
@@ -1,48 +0,0 @@
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>
)
)
}
@@ -0,0 +1,275 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi"
const configEnum = <EnumArr extends readonly string[]>(description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => {
return {
description,
defaultVal,
enumArr,
}
}
const configString = (description: string, defaultVal: string) => {
return {
description,
defaultVal,
enumArr: undefined,
}
}
// fields you can customize (don't forget 'default' - it isn't included here!)
export const configFields = [
'anthropic',
'openAI',
'greptile',
'ollama',
'openRouter',
'openAICompatible',
'azure'
] as const
const voidConfigInfo: Record<
typeof configFields[number] | 'default', {
[prop: string]: {
description: string,
enumArr?: readonly string[] | undefined,
defaultVal: string,
},
}
> = {
default: {
whichApi: configEnum(
"API Provider.",
'anthropic',
configFields,
),
maxTokens: configEnum(
"Max number of tokens to output.",
'1024',
[
"1024",
"2048",
"4096",
"8192"
] as const,
),
},
anthropic: {
apikey: configString('Anthropic API key.', ''),
model: configEnum(
"Anthropic model to use.",
'claude-3-5-sonnet-20240620',
[
"claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307"
] as const,
),
},
openAI: {
apikey: configString('OpenAI API key.', ''),
model: configEnum(
'OpenAI model to use.',
'gpt-4o',
[
"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"
] as const
),
},
greptile: {
apikey: configString('Greptile API key.', ''),
githubPAT: configString('Github PAT that Greptile uses to access your repository', ''),
remote: configEnum(
'Repo location',
'github',
[
'github',
'gitlab'
] as const
),
repository: configString('Repository identifier in "owner/repository" format.', ''),
branch: configString('Name of the branch to use.', 'main'),
},
ollama: {
endpoint: configString(
'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`.',
'http://127.0.0.1:11434'
),
// TODO we should allow user to select model inside Void, but for now we'll just let them handle the Ollama setup on their own
// model: configEnum(
// 'Ollama model to use.',
// 'llama3.1',
// ["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"] as const
// ),
},
openRouter: {
model: configString(
'OpenRouter model to use.',
'openai/gpt-4o'
),
apikey: configString('OpenRouter API key.', ''),
},
openAICompatible: {
endpoint: configString('The baseUrl (exluding /chat/completions).', 'http://127.0.0.1:11434/v1'),
model: configString('The name of the model to use.', 'gpt-4o'),
apikey: configString('Your API key.', ''),
},
azure: {
// "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."
// }
// }
// },
},
}
// this is the type that comes with metadata like desc, default val, etc
type VoidConfigInfo = typeof voidConfigInfo
export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number]
// this is the type that specifies the user's actual config
export type PartialVoidConfig = {
[K in keyof typeof voidConfigInfo]?: {
[P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal']
}
}
export type VoidConfig = {
[K in keyof typeof voidConfigInfo]: {
[P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal']
}
}
export const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => {
const config = {} as PartialVoidConfig
for (let field of [...configFields, 'default'] as const) {
config[field] = {}
for (let prop in voidConfigInfo[field]) {
config[field][prop] = currentConfig[field]?.[prop] || voidConfigInfo[field][prop].defaultVal
}
}
return config as VoidConfig
}
const defaultVoidConfig: VoidConfig = getVoidConfig({})
// 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
}
type SetConfigParamType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => void
type ConfigValueType = {
voidConfig: VoidConfig,
voidConfigInfo: VoidConfigInfo,
partialVoidConfig: PartialVoidConfig,
setConfigParam: SetConfigParamType
}
const ConfigContext = createContext<ConfigValueType>(undefined as unknown as ConfigValueType)
export function ConfigProvider({ children }: { children: ReactNode }) {
const [partialVoidConfig, setPartialVoidConfig] = useInstantState<PartialVoidConfig>({}) // the user's selections
const [voidConfig, setVoidConfig] = useState<VoidConfig>(defaultVoidConfig)
// get the config on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: 'getPartialVoidConfig' })
awaitVSCodeResponse('partialVoidConfig').then((m) => {
setPartialVoidConfig(m.partialVoidConfig)
const newFullConfig = getVoidConfig(m.partialVoidConfig)
setVoidConfig(newFullConfig)
})
}, [setPartialVoidConfig])
// return the provider
return (<ConfigContext.Provider
value={{
voidConfig,
voidConfigInfo,
partialVoidConfig: partialVoidConfig.current ?? {},
setConfigParam: (field, param, newVal) => {
const newPartialConfig: PartialVoidConfig = {
...partialVoidConfig.current,
[field]: {
...partialVoidConfig.current?.[field],
[param]: newVal
}
}
setPartialVoidConfig(newPartialConfig)
const newFullConfig = getVoidConfig(newPartialConfig)
setVoidConfig(newFullConfig)
getVSCodeAPI().postMessage({ type: 'persistPartialVoidConfig', partialVoidConfig: newPartialConfig })
}
}}
>
{children}
</ConfigContext.Provider>
)
}
export function useVoidConfig(): ConfigValueType {
const context = useContext<ConfigValueType>(ConfigContext)
if (context === undefined) {
throw new Error("useVoidConfig missing Provider")
}
return context
}
@@ -1,23 +1,28 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { ChatMessage, ChatThreads } from "../shared_types"
import { ChatMessage, ChatThreads } from "../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
type ThreadsContextValue = {
readonly allThreads: ChatThreads | null,
readonly currentThread: ChatThreads[string] | null;
// a "thread" means a chat message history
type ConfigForThreadsValueType = {
readonly getAllThreads: () => ChatThreads;
readonly getCurrentThread: () => ChatThreads[string] | null;
addMessageToHistory: (message: ChatMessage) => void;
switchToThread: (threadId: string) => void;
startNewThread: () => void;
}
const ThreadsContext = createContext<ThreadsContextValue>(undefined as unknown as ThreadsContextValue)
const ThreadsContext = createContext<ConfigForThreadsValueType>(undefined as unknown as ConfigForThreadsValueType)
const createNewThread = () => ({
id: new Date().getTime().toString(),
createdAt: new Date().toISOString(),
messages: [],
})
const createNewThread = () => {
const now = new Date().toISOString()
return {
id: new Date().getTime().toString(),
createdAt: now,
lastModified: now,
messages: [],
}
}
// const [stateRef, setState] = useInstantState(initVal)
@@ -34,12 +39,12 @@ const useInstantState = <T,>(initVal: T) => {
export function ThreadsProvider({ children }: { children: ReactNode }) {
const [allThreads, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadId, setCurrentThreadId] = useInstantState<string | null>(null)
const [allThreadsRef, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadIdRef, setCurrentThreadId] = useInstantState<string | null>(null)
// this loads allThreads in on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: "getAllThreads" })
getVSCodeAPI().postMessage({ type: 'getAllThreads' })
awaitVSCodeResponse('allThreads')
.then(response => {
setAllThreads(response.threads)
@@ -50,12 +55,12 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
return (
<ThreadsContext.Provider
value={{
allThreads: allThreads.current,
currentThread: currentThreadId.current === null || allThreads.current === null ? null : allThreads.current[currentThreadId.current],
getAllThreads: () => allThreadsRef.current ?? {},
getCurrentThread: () => currentThreadIdRef.current ? allThreadsRef.current?.[currentThreadIdRef.current] ?? null : null,
addMessageToHistory: (message: ChatMessage) => {
let currentThread: ChatThreads[string]
if (!(currentThreadId.current === null || allThreads.current === null)) {
currentThread = allThreads.current[currentThreadId.current]
if (!(currentThreadIdRef.current === null || allThreadsRef.current === null)) {
currentThread = allThreadsRef.current[currentThreadIdRef.current]
}
else {
currentThread = createNewThread()
@@ -63,9 +68,10 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
}
setAllThreads({
...allThreads.current,
...allThreadsRef.current,
[currentThread.id]: {
...currentThread,
lastModified: new Date().toISOString(),
messages: [...currentThread.messages, message],
}
})
@@ -78,7 +84,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
startNewThread: () => {
const newThread = createNewThread()
setAllThreads({
...allThreads.current,
...allThreadsRef.current,
[newThread.id]: newThread
})
setCurrentThreadId(newThread.id)
@@ -90,10 +96,10 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
)
}
export function useThreads(): ThreadsContextValue {
const context = useContext<ThreadsContextValue>(ThreadsContext)
export function useThreads(): ConfigForThreadsValueType {
const context = useContext<ConfigForThreadsValueType>(ThreadsContext)
if (context === undefined) {
throw new Error("useThreads must be used within a ThreadsProvider")
throw new Error("useThreads missing Provider")
}
return context
}
+54 -19
View File
@@ -1,48 +1,83 @@
import { Command, WebviewMessage } from "../shared_types";
import { useEffect } from "react";
import { MessageFromSidebar, MessageToSidebar, } from "../common/shared_types";
import { v4 as uuidv4 } from 'uuid';
type Command = MessageToSidebar['type']
// message -> res[]
const awaiting: { [c in Command]: ((res: any) => void)[] } = {
// messageType -> res[]
const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = {
"ctrl+l": [],
"applyChanges": [],
"requestFiles": [],
"ctrl+k": [],
"files": [],
"apiConfig": [],
"getApiConfig": [],
"partialVoidConfig": [],
"startNewThread": [],
"getAllThreads": [],
"allThreads": [],
"persistThread": [],
"toggleThreadSelector": []
"toggleThreadSelector": [],
"toggleSettings": [],
"deviceId": [],
}
// messageType -> id -> res
const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = {
"ctrl+l": {},
"ctrl+k": {},
"files": {},
"partialVoidConfig": {},
"startNewThread": {},
"allThreads": {},
"toggleThreadSelector": {},
"toggleSettings": {},
"deviceId": {}
}
// use this function to await responses
export const awaitVSCodeResponse = <C extends Command>(c: C) => {
let result: Promise<WebviewMessage & { type: C }> = new Promise((res, rej) => {
awaiting[c].push(res)
let result: Promise<MessageToSidebar & { type: C }> = new Promise((res, rej) => {
onetimeCallbacks[c].push(res)
})
return result
}
export const resolveAwaitingVSCodeResponse = (m: WebviewMessage) => {
// resolve all promises for this message
for (let res of awaiting[m.type]) {
// use this function to add a listener to a certain type of message
export const useOnVSCodeMessage = <C extends Command>(messageType: C, fn: (e: MessageToSidebar & { type: C }) => void) => {
useEffect(() => {
const mType = messageType
const callbackId: string = uuidv4();
// @ts-ignore
callbacks[mType][callbackId] = fn;
return () => { delete callbacks[mType][callbackId] }
}, [messageType, fn])
}
// this function gets called whenever sidebar receives a message - it should only mount once
export const onMessageFromVSCode = (m: MessageToSidebar) => {
// resolve all promises for this message type
for (let res of onetimeCallbacks[m.type]) {
res(m)
onetimeCallbacks[m.type].splice(0) // clear the array
}
// call the listener for this message type
for (let res of Object.values(callbacks[m.type])) {
res(m)
awaiting[m.type].splice(0) // clear the array
}
}
// VS Code exposes the function acquireVsCodeApi() to us, it should only get called once
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
type AcquireVsCodeApiType = () => {
postMessage(message: WebviewMessage): void;
postMessage(message: MessageFromSidebar): void;
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
// getState(): any;
};
// VS Code exposes the function acquireVsCodeApi() to us, this variable makes sure it only gets called once
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
export function getVSCodeAPI(): ReturnType<AcquireVsCodeApiType> {
if (vsCodeApi)
return vsCodeApi;
+57 -11
View File
@@ -1,20 +1,66 @@
import * as React from "react"
import { useEffect } from "react"
import * as ReactDOM from "react-dom/client"
import Sidebar from "./Sidebar"
import { ThreadsProvider } from "./threadsContext"
import { CtrlK } from "./CtrlK"
import { ThreadsProvider } from "./contextForThreads"
import { ConfigProvider } from "./contextForConfig"
import { MessageToSidebar } from "../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi"
import { identifyUser, initPosthog } from "./metrics/posthog"
// mount the sidebar on the id="root" element
if (typeof document === "undefined") {
console.log("index.tsx error: document was undefined")
}
const rootElement = document.getElementById("root")!
console.log("Void root Element:", rootElement)
const extension = (
<ThreadsProvider>
<Sidebar />
</ThreadsProvider>
)
const root = ReactDOM.createRoot(rootElement)
root.render(extension)
const CommonEffects = () => {
// initialize posthog
useEffect(() => {
initPosthog()
}, [])
// when we get the deviceid, identify the user
useEffect(() => {
getVSCodeAPI().postMessage({ type: 'getDeviceId' });
awaitVSCodeResponse('deviceId').then((m => {
identifyUser(m.deviceId)
}))
}, [])
// Receive messages from the VSCode extension
useEffect(() => {
const listener = (event: MessageEvent) => {
const m = event.data as MessageToSidebar;
onMessageFromVSCode(m)
}
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener)
}, [])
return null
}
(() => {
// mount the sidebar on the id="root" element
const rootElement = document.getElementById("root")!
console.log("Void root Element:", rootElement)
const sidebar = (<>
<CommonEffects />
<ThreadsProvider>
<ConfigProvider>
<Sidebar />
</ConfigProvider>
</ThreadsProvider>
<ConfigProvider>
<CtrlK />
</ConfigProvider>
</>)
const root = ReactDOM.createRoot(rootElement)
root.render(sidebar)
})();
@@ -1,79 +1,41 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { getVSCodeAPI } from "../getVscodeApi"
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000
const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
// 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)
const customStyle = {
...atomOneDarkReasonable,
'code[class*="language-"]': {
...atomOneDarkReasonable['code[class*="language-"]'],
background: "none",
},
}
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
}, COPY_FEEDBACK_TIMEOUT)
}
}, [copyButtonState])
return (<>
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
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>
{!toolbar ? null : (
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className="flex space-x-2 p-2">{buttonsOnHover === null ? null : buttonsOnHover}</div>
</div>
)}
<div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg`}
>
<pre className="p-2">{text}</pre>
<SyntaxHighlighter
language={language ?? 'plaintext'} // TODO must auto detect language
style={customStyle}
className={"rounded-sm"}
>
{text}
</SyntaxHighlighter>
</div>
</div>
</>
)
}
@@ -1,6 +1,57 @@
import React, { JSX } from "react"
import React, { JSX, useCallback, useEffect, useState } from "react"
import { marked, MarkedToken, Token, TokensList } from "marked"
import BlockCode from "./BlockCode"
import { getVSCodeAPI } from "../getVscodeApi"
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ text }: { text: 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])
return <>
<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>
</>
}
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
@@ -12,7 +63,11 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
}
if (t.type === "code") {
return <BlockCode text={t.text} />
return <BlockCode
text={t.text}
language={t.lang}
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
/>
}
if (t.type === "heading") {
@@ -165,4 +220,4 @@ const MarkdownRender = ({ string, nested = false }: { string: string, nested?: b
)
}
export default MarkdownRender
export default MarkdownRender
@@ -0,0 +1,20 @@
import posthog from 'posthog-js'
export const identifyUser = (id: string) => {
posthog.identify(id)
}
export const captureEvent = (eventId: string, properties: object) => {
posthog.capture(eventId, properties)
}
export const initPosthog = () => {
// We send absolutely no code to the server. We only track usage metrics like button clicks, etc. This might change and we might eventually add an opt-in or opt-out.
posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2',
{
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar
}
)
}
+6 -1
View File
@@ -29,11 +29,16 @@ html {
}
}
/* add transparency when disabled */
&: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 focus:outline-vscode-focus-border;
}
.dropdown {
@apply bg-vscode-dropdown-bg text-vscode-dropdown-foreground border-vscode-dropdown-border focus:outline-vscode-focus-border;
}
+5 -2
View File
@@ -18,8 +18,11 @@ module.exports = {
"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)",
"button-secondary-hoverBg": "var(--vscode-button-secondaryHoverBackground)",
"dropdown-bg": "var(--vscode-settings-dropdownBackground)",
"dropdown-foreground": "var(--vscode-settings-dropdownForeground)",
"dropdown-border": "var(--vscode-settings-dropdownBorder)",
"focus-border": "var(--vscode-focusBorder)",
},
},
},
+751 -100
View File
File diff suppressed because it is too large Load Diff
@@ -823,7 +823,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
return extHostWebviewPanels.createWebviewPanel(extension, viewType, title, showOptions, options);
},
createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): vscode.WebviewEditorInset {
checkProposedApiEnabled(extension, 'editorInsets');
// checkProposedApiEnabled(extension, 'editorInsets'); // Void commented this out
return extHostEditorInsets.createWebviewEditorInset(editor, line, height, options, extension);
},
createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal {
-1
View File
@@ -9479,7 +9479,6 @@ declare module 'vscode' {
*/
readonly extensionHostPort: number;
}
/**
* Content settings for a webview.
*/