Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c01b1e9488 | |||
| c3a934777d | |||
| 3aba62a6ed | |||
| ad5128f885 | |||
| 376172e5a9 | |||
| 4dd6553e34 | |||
| f2b3a2be30 | |||
| 69277a287a | |||
| 6c0f17d8f9 | |||
| 6dc9988af0 | |||
| 18656372f8 | |||
| febf492790 | |||
| bcb0c33616 | |||
| 7072933a88 | |||
| f2342bee2d | |||
| 572e9f07ea | |||
| 09d84044b5 | |||
| a49a1324a5 | |||
| 8538caeffc | |||
| 79ecd98818 | |||
| c5b5e07a98 | |||
| 2addd06e60 | |||
| 177f2e92f8 | |||
| 217f61902e | |||
| 9274602051 | |||
| efa3b033af | |||
| 18649d3255 | |||
| 2b55d94cce | |||
| 2d17133684 | |||
| e2e6cb0327 | |||
| 5415f1e3cb | |||
| 0f3a79ab54 | |||
| 983e926a23 | |||
| 302aa2b384 | |||
| 84ff9784ca | |||
| 6dfab6d00c | |||
| 628ed9a578 | |||
| 0a6f7b8ebc | |||
| 5b511edca1 | |||
| 4ecbd998bd | |||
| fcd0eb30c6 | |||
| aa4ce3478a | |||
| 1e82781c52 | |||
| edf9dc9279 | |||
| 63c6b6bec5 | |||
| 154a82ab0b | |||
| ab51e97008 | |||
| 0240fc1803 | |||
| d3b862ae91 | |||
| dc2344fd4c | |||
| 0072c554fb | |||
| ed8381c6cb | |||
| 40b4ecb4f8 | |||
| 5268ededbb | |||
| a69e1b940f | |||
| 15994c46d1 | |||
| b565a91194 | |||
| b30937934a | |||
| eec7cd7321 | |||
| f86b84dcc6 | |||
| cd9bca0451 | |||
| ce3043b595 | |||
| 68c5ebb000 | |||
| 08e91e921b | |||
| 6c2ca794f5 | |||
| b3fd408cf7 | |||
| dc96d126bb | |||
| 07fd622901 | |||
| bb74cbb56b | |||
| ffab60b4b8 | |||
| 2309fc53fb | |||
| e153ffee4e | |||
| a3d741d685 | |||
| a12e35c5f0 | |||
| 5f3e664eed | |||
| fb69afd5c4 | |||
| 8c9eeaa133 | |||
| 00f59e9555 | |||
| 767c45080f | |||
| b0758e02aa | |||
| 7f81836d9f | |||
| 33c3ef6751 | |||
| 20056f209e | |||
| 69d848f22b | |||
| 1557a44934 | |||
| 89dd63a0ee | |||
| e5c55145e9 | |||
| 2638d62a24 | |||
| e04d839640 | |||
| 0158e2fd5d | |||
| 3e308f2082 | |||
| 429224993b | |||
| 1b8271d217 | |||
| 29ef6b5b43 | |||
| 23a42659b3 | |||
| 9d7a5eccd3 | |||
| 42bdcf54ca | |||
| 251b65d04d | |||
| b72da01632 | |||
| e2f337f882 | |||
| 496c0178de | |||
| ef94de48c2 | |||
| 14a8b0ee4e | |||
| 38d31ae737 | |||
| d3beafec74 | |||
| 73fb5bb3c6 | |||
| 247a3b7feb | |||
| 2e5be73f0c | |||
| 2ca1db35af | |||
| 11baebdf54 | |||
| 8dde88222c | |||
| 2b47e9745f | |||
| f96416be38 | |||
| a4ef0de0ac | |||
| c3e2dc1621 | |||
| f7a905e27f | |||
| c579bcc2fd | |||
| ad7b7a6866 | |||
| bed7929e48 | |||
| b07842ec3f | |||
| 19b2b8bd77 | |||
| f4a407d80f | |||
| 95be6e0b37 | |||
| 6741a6fceb | |||
| db560744fb | |||
| 8937b52e72 | |||
| 7550b96389 | |||
| 8aa3282c3d | |||
| 9ed68d5fac | |||
| 7668d3dee7 | |||
| fc65d847c4 | |||
| 653d5e9c1f | |||
| 631f724138 | |||
| 162bf704cc | |||
| d64e9c93f5 | |||
| 74c610632e | |||
| 2496a2c42e | |||
| 656b24b701 | |||
| 3fd9b572fe | |||
| dab75a6614 | |||
| 4747691e74 | |||
| 6dac9626c0 | |||
| 180df75bb4 | |||
| b8a5e5b67c | |||
| 5039ca8fdf | |||
| aea35c95f2 | |||
| 0a5ebc1c50 | |||
| cbe3d4bd45 | |||
| 16b84ec933 | |||
| 2661f5e8e1 | |||
| 760f8c447c | |||
| 57db928f7a | |||
| 9b130edcfe | |||
| ff0dcd5095 | |||
| 30b528402c | |||
| 6d513a1e60 | |||
| 7269c4e582 | |||
| 083bbf7d9f | |||
| e54f49ebfb | |||
| 31cd10b9e6 | |||
| c6dcb8eb9e | |||
| 3a98d1edb1 | |||
| 22d32aca3a | |||
| fe5e02c3a9 | |||
| 3d58627f30 | |||
| 20fd8f318b | |||
| 98ee94018e | |||
| ac9bc80e20 | |||
| 8203b42169 | |||
| bc027b2fca | |||
| afa24231e6 | |||
| da5bb4b68e | |||
| f2d741d01f | |||
| e893dc0327 | |||
| e05a399177 | |||
| 4f285ce364 | |||
| 2be5a2d13a | |||
| 5e5fe80901 | |||
| 44bc8bfdbf | |||
| 4f9b5ac873 | |||
| 1beb011bfa | |||
| bf9029f9bc | |||
| d7a3c44e31 | |||
| 7b31f2841a | |||
| e8cf6ee9e7 | |||
| 2b3f828654 | |||
| 7ee1840e77 | |||
| 1843cd0878 | |||
| 78e10a5593 | |||
| 7bcefd4536 | |||
| 6d5f885306 | |||
| 99adb23149 | |||
| 043a38bd36 | |||
| 0269411c16 | |||
| f4ceb53738 |
+53
-48
@@ -4,7 +4,7 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a
|
||||
|
||||
There are two main ways to contribute:
|
||||
|
||||
- Suggest New Features ([discord](https://discord.gg/4GAxHVAD))
|
||||
- Suggest New Features ([discord](https://discord.gg/RSNjgaugJs))
|
||||
- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3))
|
||||
|
||||
We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).
|
||||
@@ -20,10 +20,10 @@ 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):
|
||||
2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, *don't* just cd into it):
|
||||
|
||||
```
|
||||
open /extensions/void
|
||||
open /extensions/void in vscode
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
@@ -40,111 +40,116 @@ 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".
|
||||
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".
|
||||
|
||||
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!
|
||||
|
||||
## 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`).
|
||||
|
||||
Make sure you're on the correct NodeJS version as per `.nvmrc`.
|
||||
|
||||
### a. Building on a Mac
|
||||
|
||||
To build on a Mac, open `void/` in VSCode. Make sure you've built the extension (`cd ./extensions/void && 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.
|
||||
|
||||
```
|
||||
yarn
|
||||
npm install
|
||||
```
|
||||
|
||||
2. In VS Code, press <kbd>Ctrl+Shift+B</kbd> to start the build process - this can take some time. If you're not using VS Code, run `npm run watch` instead.
|
||||
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-win32-x64`. 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.
|
||||
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)`
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
### 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.
|
||||
If you're on Windows, we recommend running the project inside a dev container. VSCode should prompt you to do this automatically.
|
||||
|
||||
## 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.
|
||||
|
||||
# Submitting a Pull Request
|
||||
|
||||
When you've made changes and want to submit them, please submit a pull request.
|
||||
Please submit a pull request once you've made a change. Here are a few guidelines:
|
||||
|
||||
- A PR should be about one *single* feature change. The fewer items you change, the more likely the PR is to be accepted.
|
||||
|
||||
- Your PR should contain a description that first explains at a high level what you did, and then describes the exact changes you made (and to which files). Please don't use vague statements like "refactored code" or "improved types" (instead, describe what code you refactored, or what types you changed).
|
||||
|
||||
- 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"**/.DS_Store": true,
|
||||
"**/Thumbs.db": true,
|
||||
"out": false,
|
||||
"**/node_modules": false
|
||||
"**/node_modules": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
Please see the `CONTRIBUTING.md` for information on how to contribute :)!
|
||||
|
||||
|
||||
Here's an overview on how the extension works:
|
||||
|
||||
- The extension mounts in `extension.ts`.
|
||||
|
||||
- The Sidebar's HTML (everything in `sidebar/`) is built in React, and it's rendered by mounting a `<script>` tag - see `SidebarWebviewProvider.ts`.
|
||||
|
||||
- Communication between the sidebar script and the extension takes place via API. You can search for "postMessage" to see where API calls happen.
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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));
|
||||
|
||||
Generated
+678
-178
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "void",
|
||||
"publisher": "void",
|
||||
"displayName": "Void",
|
||||
"description": "",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.89.0"
|
||||
"vscode": "^1.92.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
@@ -13,39 +14,8 @@
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "API Keys",
|
||||
"properties": {
|
||||
"void.whichApi": {
|
||||
"type": "string",
|
||||
"default": "anthropic",
|
||||
"description": "Choose a model to use (anthropic | openai | greptile | ollama)"
|
||||
},
|
||||
"void.anthropicApiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Anthropic API Key"
|
||||
},
|
||||
"void.openAIApiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "OpenAI API Key"
|
||||
},
|
||||
"void.greptileApiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Greptile API Key"
|
||||
},
|
||||
"void.githubPAT": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Greptile - Github PAT (gives Greptile access to your repo)"
|
||||
},
|
||||
"void.ollamaSettings": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Ollama settings (coming soon...)"
|
||||
}
|
||||
}
|
||||
"title": "Void",
|
||||
"properties": {}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
@@ -54,15 +24,30 @@
|
||||
},
|
||||
{
|
||||
"command": "void.ctrl+k",
|
||||
"title": "Show Selection Lens"
|
||||
"title": "Make Inline Edit"
|
||||
},
|
||||
{
|
||||
"command": "void.approveDiff",
|
||||
"command": "void.acceptDiff",
|
||||
"title": "Approve Diff"
|
||||
},
|
||||
{
|
||||
"command": "void.discardDiff",
|
||||
"command": "void.rejectDiff",
|
||||
"title": "Discard Diff"
|
||||
},
|
||||
{
|
||||
"command": "void.startNewThread",
|
||||
"title": "Start a new chat",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "void.toggleThreadSelector",
|
||||
"title": "View past chats",
|
||||
"icon": "$(history)"
|
||||
},
|
||||
{
|
||||
"command": "void.toggleSettings",
|
||||
"title": "Void settings",
|
||||
"icon": "$(settings-gear)"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
@@ -94,48 +79,75 @@
|
||||
"key": "ctrl+k",
|
||||
"mac": "cmd+k"
|
||||
}
|
||||
]
|
||||
],
|
||||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
"command": "void.startNewThread",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "void.toggleThreadSelector",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "void.toggleSettings",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"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/mocha": "^10.0.8",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.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": "^6.0.0-beta",
|
||||
"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",
|
||||
"marked": "^14.1.0",
|
||||
"ollama": "^0.5.8",
|
||||
"ollama": "^0.5.9",
|
||||
"openai": "^4.68.1",
|
||||
"postcss": "^8.4.41",
|
||||
"posthog-js": "^1.174.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",
|
||||
"openai": "^4.57.0"
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { SuggestedEdit } from './getDiffedLines';
|
||||
|
||||
// each diff on the user's screen right now
|
||||
type DiffType = {
|
||||
diffid: number,
|
||||
lenses: vscode.CodeLens[],
|
||||
greenRange: vscode.Range,
|
||||
originalCode: string, // If a revert happens, we replace the greenRange with this content.
|
||||
}
|
||||
|
||||
// TODO in theory this should be disposed
|
||||
const greenDecoration = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: 'rgba(0 255 51 / 0.2)',
|
||||
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
|
||||
})
|
||||
|
||||
export class ApprovalCodeLensProvider implements vscode.CodeLensProvider {
|
||||
|
||||
private _diffsOfDocument: { [docUriStr: string]: DiffType[] } = {};
|
||||
private _computedLensesOfDocument: { [docUriStr: string]: vscode.CodeLens[] } = {} // computed from diffsOfDocument[docUriStr].lenses
|
||||
private _diffidPool = 0
|
||||
|
||||
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
|
||||
|
||||
private _weAreEditing: boolean = false
|
||||
|
||||
// used internally by vscode
|
||||
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
|
||||
|
||||
|
||||
// used internally by vscode
|
||||
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
|
||||
const docUriStr = document.uri.toString()
|
||||
return this._computedLensesOfDocument[docUriStr]
|
||||
}
|
||||
|
||||
// declared by us, registered with vscode.languages.registerCodeLensProvider()
|
||||
constructor() {
|
||||
// this acts as a useEffect. Every time text changes, clear the diffs in this editor
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
if (this._weAreEditing)
|
||||
return
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
this._diffsOfDocument[docUriStr].splice(0) // clear diffs
|
||||
editor.setDecorations(greenDecoration, []) // clear decorations
|
||||
this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute
|
||||
this._onDidChangeCodeLenses.fire() // refresh
|
||||
})
|
||||
}
|
||||
|
||||
// used by us only
|
||||
private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => {
|
||||
editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh highlighting
|
||||
this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute _computedLensesOfDocument (can optimize this later)
|
||||
this._onDidChangeCodeLenses.fire() // fire event for vscode to refresh lenses
|
||||
}
|
||||
|
||||
// used by us only
|
||||
public async addNewApprovals(editor: vscode.TextEditor, suggestedEdits: SuggestedEdit[]) {
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
if (!this._diffsOfDocument[docUriStr])
|
||||
this._diffsOfDocument[docUriStr] = []
|
||||
if (!this._computedLensesOfDocument[docUriStr])
|
||||
this._computedLensesOfDocument[docUriStr] = []
|
||||
|
||||
|
||||
// 1. convert suggested edits (which are described using line numbers) into actual edits (described using vscode.Range, vscode.Uri)
|
||||
// must do this before adding codelenses or highlighting so that codelens and highlights will apply to the fresh code and not the old code
|
||||
// apply changes in reverse order so additions don't push down the line numbers of the next edit
|
||||
let workspaceEdit = new vscode.WorkspaceEdit();
|
||||
for (let i = suggestedEdits.length - 1; i > -1; i -= 1) {
|
||||
let suggestedEdit = suggestedEdits[i]
|
||||
|
||||
let greenRange: vscode.Range
|
||||
|
||||
// INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1})
|
||||
if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) {
|
||||
const originalPosition = new vscode.Position(suggestedEdit.originalStartLine, 0)
|
||||
workspaceEdit.insert(docUri, originalPosition, suggestedEdit.newContent + '\n') // add back in the line we deleted when we made the startline->endline range go negative
|
||||
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine + 1, 0)
|
||||
}
|
||||
// DELETIONS
|
||||
else if (suggestedEdit.startLine > suggestedEdit.endLine) {
|
||||
const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0)
|
||||
workspaceEdit.delete(docUri, deleteRange)
|
||||
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.startLine, 0)
|
||||
suggestedEdit.originalContent += '\n' // add back in the line we deleted when we made the startline->endline range go negative
|
||||
}
|
||||
// REPLACEMENTS
|
||||
else {
|
||||
const originalRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine, Number.MAX_SAFE_INTEGER)
|
||||
workspaceEdit.replace(docUri, originalRange, suggestedEdit.newContent)
|
||||
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine, Number.MAX_SAFE_INTEGER)
|
||||
}
|
||||
|
||||
this._diffsOfDocument[docUriStr].push({
|
||||
diffid: this._diffidPool,
|
||||
greenRange: greenRange,
|
||||
originalCode: suggestedEdit.originalContent,
|
||||
lenses: [
|
||||
new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.approveDiff', arguments: [{ diffid: this._diffidPool }] }),
|
||||
new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.discardDiff', arguments: [{ diffid: this._diffidPool }] })
|
||||
]
|
||||
});
|
||||
this._diffidPool += 1
|
||||
}
|
||||
|
||||
this._weAreEditing = true
|
||||
await vscode.workspace.applyEdit(workspaceEdit)
|
||||
await vscode.workspace.save(docUri)
|
||||
this._weAreEditing = false
|
||||
|
||||
// refresh
|
||||
this.refreshLenses(editor, docUriStr)
|
||||
|
||||
console.log('diffs after added:', this._diffsOfDocument[docUriStr])
|
||||
}
|
||||
|
||||
// called on void.approveDiff
|
||||
public async approveDiff({ diffid }: { diffid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// get index of this diff in diffsOfDocument
|
||||
const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
|
||||
if (index === -1) {
|
||||
console.error('Error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr])
|
||||
return
|
||||
}
|
||||
|
||||
// remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history)
|
||||
this._diffsOfDocument[docUriStr].splice(index, 1)
|
||||
|
||||
// refresh
|
||||
this.refreshLenses(editor, docUriStr)
|
||||
}
|
||||
|
||||
|
||||
// called on void.discardDiff
|
||||
public async discardDiff({ diffid }: { diffid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// get index of this diff in diffsOfDocument
|
||||
const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
|
||||
if (index === -1) {
|
||||
console.error('Void error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr])
|
||||
return
|
||||
}
|
||||
|
||||
const { greenRange: range, lenses, originalCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index
|
||||
|
||||
// remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history)
|
||||
this._diffsOfDocument[docUriStr].splice(index, 1)
|
||||
|
||||
// clear the decoration in this diffs range
|
||||
editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange))
|
||||
|
||||
// REVERT THE CHANGE (this is the only part that's different from approveDiff)
|
||||
let workspaceEdit = new vscode.WorkspaceEdit();
|
||||
workspaceEdit.replace(docUri, range, originalCode);
|
||||
this._weAreEditing = true
|
||||
await vscode.workspace.applyEdit(workspaceEdit)
|
||||
await vscode.workspace.save(docUri)
|
||||
this._weAreEditing = false
|
||||
|
||||
// refresh
|
||||
this.refreshLenses(editor, docUriStr)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class CtrlKCodeLensProvider implements vscode.CodeLensProvider {
|
||||
|
||||
private codelensesOfDocument: { [documentUri: string]: vscode.CodeLens[] } = {};
|
||||
|
||||
// only called by vscode's internals
|
||||
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
|
||||
const docUri = document.uri.toString()
|
||||
return this.codelensesOfDocument[docUri];
|
||||
}
|
||||
|
||||
// only called by us
|
||||
public addNewCodeLens(document: vscode.TextDocument, selection: vscode.Selection) {
|
||||
|
||||
const docUri = document.uri.toString()
|
||||
|
||||
if (!this.codelensesOfDocument[docUri])
|
||||
this.codelensesOfDocument[docUri] = []
|
||||
|
||||
// if any other codelens intersects with the selection, don't do it (and have the user now focus that codelens)
|
||||
for (let lens of this.codelensesOfDocument[docUri]) {
|
||||
if (lens.range.intersection(selection))
|
||||
return
|
||||
}
|
||||
|
||||
this.codelensesOfDocument[docUri] = [
|
||||
...this.codelensesOfDocument[docUri],
|
||||
new vscode.CodeLens(new vscode.Range(selection.start.line, 0, selection.end.line, Infinity), { title: '', command: '' })];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { findDiffs } from './findDiffs';
|
||||
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './common/shared_types';
|
||||
|
||||
|
||||
|
||||
// TODO in theory this should be disposed
|
||||
const greenDecoration = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: 'rgba(0 255 51 / 0.2)',
|
||||
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
|
||||
})
|
||||
|
||||
|
||||
// responsible for displaying diffs and showing accept/reject buttons
|
||||
export class DisplayChangesProvider implements vscode.CodeLensProvider {
|
||||
|
||||
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
|
||||
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
|
||||
|
||||
private _diffareaidPool = 0
|
||||
private _diffidPool = 0
|
||||
private _weAreEditing: boolean = false
|
||||
|
||||
// used internally by vscode
|
||||
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
|
||||
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
|
||||
|
||||
// used internally by vscode
|
||||
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
|
||||
const docUriStr = document.uri.toString()
|
||||
return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
|
||||
}
|
||||
|
||||
// declared by us, registered with vscode.languages.registerCodeLensProvider()
|
||||
constructor() {
|
||||
|
||||
console.log('Creating DisplayChangesProvider')
|
||||
|
||||
// this acts as a useEffect. Every time text changes, clear the diffs in this editor
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
|
||||
if (!editor)
|
||||
return
|
||||
if (this._weAreEditing)
|
||||
return
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
|
||||
|
||||
// loop through each change
|
||||
for (const change of e.contentChanges) {
|
||||
|
||||
// here, `change.range` is the range of the original file that gets replaced with `change.text`
|
||||
|
||||
|
||||
// compute net number of newlines lines that were added/removed
|
||||
const numNewLines = (change.text.match(/\n/g) || []).length
|
||||
const numLineDeletions = change.range.end.line - change.range.start.line
|
||||
const deltaNewlines = numNewLines - numLineDeletions
|
||||
|
||||
// compute overlap with each diffArea and shrink/elongate the diffArea accordingly
|
||||
for (const diffArea of diffAreas) {
|
||||
|
||||
// if the change is fully within the diffArea, elongate it by the delta amount of newlines
|
||||
if (change.range.start.line >= diffArea.startLine && change.range.end.line <= diffArea.endLine) {
|
||||
diffArea.endLine += deltaNewlines
|
||||
}
|
||||
// check if the `diffArea` was fully deleted and remove it if so
|
||||
if (diffArea.startLine > diffArea.endLine) {
|
||||
//remove it
|
||||
const index = diffAreas.findIndex(da => da === diffArea)
|
||||
diffAreas.splice(index, 1)
|
||||
}
|
||||
|
||||
// TODO handle other cases where eg. the change overlaps many diffAreas
|
||||
}
|
||||
|
||||
|
||||
// if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
|
||||
for (const diffArea of diffAreas) {
|
||||
if (diffArea.startLine > change.range.end.line) {
|
||||
diffArea.startLine += deltaNewlines
|
||||
diffArea.endLine += deltaNewlines
|
||||
}
|
||||
}
|
||||
|
||||
// TODO merge any diffAreas if they overlap with each other as a result from the shift
|
||||
|
||||
}
|
||||
|
||||
// refresh the diffAreas
|
||||
this.refreshDiffAreas(docUri)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// used by us only
|
||||
public addDiffArea(uri: vscode.Uri, diffArea: BaseDiffArea) {
|
||||
|
||||
const uriStr = uri.toString()
|
||||
|
||||
// make sure array is defined
|
||||
if (!this._diffAreasOfDocument[uriStr])
|
||||
this._diffAreasOfDocument[uriStr] = []
|
||||
|
||||
// remove all diffAreas that the new `diffArea` is overlapping with
|
||||
this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
|
||||
|
||||
const noOverlap = da.startLine > diffArea.endLine || da.endLine < diffArea.startLine
|
||||
|
||||
if (!noOverlap) return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// add `diffArea` to storage
|
||||
this._diffAreasOfDocument[uriStr].push({
|
||||
...diffArea,
|
||||
diffareaid: this._diffareaidPool
|
||||
})
|
||||
this._diffareaidPool += 1
|
||||
}
|
||||
|
||||
|
||||
// used by us only
|
||||
public refreshDiffAreas(docUri: vscode.Uri) {
|
||||
|
||||
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
|
||||
if (!editor) {
|
||||
console.log('Error: No active editor!')
|
||||
return;
|
||||
}
|
||||
|
||||
const docUriStr = docUri.toString()
|
||||
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
|
||||
|
||||
// reset all diffs (we update them below)
|
||||
this._diffsOfDocument[docUriStr] = []
|
||||
|
||||
// for each diffArea
|
||||
for (const diffArea of diffAreas) {
|
||||
|
||||
// get code inside of diffArea
|
||||
const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n')
|
||||
|
||||
// compute the diffs
|
||||
const diffs = findDiffs(diffArea.originalCode, currentCode)
|
||||
|
||||
// print diffs
|
||||
console.log('!CODEBefore:', JSON.stringify(diffArea.originalCode))
|
||||
console.log('!CODEAfter:', JSON.stringify(currentCode))
|
||||
|
||||
// add the diffs to `this._diffsOfDocument[docUriStr]`
|
||||
this.addDiffs(editor.document.uri, diffs, diffArea)
|
||||
|
||||
for (const diff of this._diffsOfDocument[docUriStr]) {
|
||||
console.log('------------')
|
||||
console.log('deletedCode:', JSON.stringify(diff.deletedCode))
|
||||
console.log('insertedCode:', JSON.stringify(diff.insertedCode))
|
||||
console.log('deletedRange:', diff.deletedRange.start.line, diff.deletedRange.end.line,)
|
||||
console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// update green highlighting
|
||||
editor.setDecorations(
|
||||
greenDecoration,
|
||||
(this._diffsOfDocument[docUriStr]
|
||||
.filter(diff => diff.insertedRange !== undefined)
|
||||
.map(diff => diff.insertedRange)
|
||||
)
|
||||
);
|
||||
|
||||
// TODO update red highlighting
|
||||
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
|
||||
|
||||
// update code lenses
|
||||
this._onDidChangeCodeLenses.fire()
|
||||
|
||||
}
|
||||
|
||||
// used by us only
|
||||
public addDiffs(docUri: vscode.Uri, diffs: BaseDiff[], diffArea: DiffArea) {
|
||||
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// if no diffs, set diffs to []
|
||||
if (!this._diffsOfDocument[docUriStr])
|
||||
this._diffsOfDocument[docUriStr] = []
|
||||
|
||||
// add each diff and its codelens to the document
|
||||
for (let i = diffs.length - 1; i > -1; i -= 1) {
|
||||
let suggestedDiff = diffs[i]
|
||||
|
||||
this._diffsOfDocument[docUriStr].push({
|
||||
...suggestedDiff,
|
||||
diffid: this._diffidPool,
|
||||
// originalCode: suggestedDiff.deletedText,
|
||||
lenses: [
|
||||
new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
|
||||
new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
|
||||
]
|
||||
});
|
||||
this._diffidPool += 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// called on void.acceptDiff
|
||||
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
// get document uri
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// get relevant diff
|
||||
// TODO speed up with hashmap
|
||||
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
|
||||
if (diffIdx === -1) {
|
||||
console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
|
||||
}
|
||||
|
||||
// get relevant diffArea
|
||||
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
|
||||
if (diffareaIdx === -1) {
|
||||
console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
|
||||
}
|
||||
|
||||
const diff = this._diffsOfDocument[docUriStr][diffIdx]
|
||||
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
|
||||
|
||||
// replace `originalCode[diff.deletedRange]` with diff.insertedCode
|
||||
// TODO add a history event to undo this change
|
||||
const originalLines = diffArea.originalCode.split('\n');
|
||||
const relativeStart = diff.deletedRange.start.line - diffArea.originalStartLine
|
||||
const relativeEnd = diff.deletedRange.end.line - diffArea.originalStartLine
|
||||
diffArea.originalCode = [
|
||||
...originalLines.slice(0, relativeStart), // lines before the deleted range
|
||||
...diff.insertedCode.split('\n'), // inserted lines
|
||||
...originalLines.slice(relativeEnd + 1) // lines after the deleted range
|
||||
].join('\n')
|
||||
|
||||
// if the diffArea has no changes, remove it
|
||||
const currentDiffAreaCode = editor.document.getText()
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.slice(diffArea.startLine, diffArea.endLine + 1)
|
||||
.join('\n')
|
||||
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
|
||||
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
|
||||
this._diffAreasOfDocument[docUriStr].splice(index, 1)
|
||||
}
|
||||
|
||||
// refresh the diff area
|
||||
this.refreshDiffAreas(docUri)
|
||||
}
|
||||
|
||||
|
||||
// called on void.rejectDiff
|
||||
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
// get document uri
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// get relevant diff
|
||||
// TODO speed up with hashmap
|
||||
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
|
||||
if (diffIdx === -1) {
|
||||
console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
|
||||
}
|
||||
|
||||
// get relevant diffArea
|
||||
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
|
||||
if (diffareaIdx === -1) {
|
||||
console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
|
||||
}
|
||||
|
||||
const diff = this._diffsOfDocument[docUriStr][diffIdx]
|
||||
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
|
||||
|
||||
// replace `editorCode[diff.insertedRange]` with diff.deletedCode
|
||||
const workspaceEdit = new vscode.WorkspaceEdit();
|
||||
workspaceEdit.replace(docUri, diff.insertedRange, diff.deletedCode)
|
||||
this._weAreEditing = true
|
||||
await vscode.workspace.applyEdit(workspaceEdit)
|
||||
this._weAreEditing = false
|
||||
|
||||
// if the diffArea has no changes, remove it
|
||||
const currentDiffAreaCode = editor.document.getText()
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.slice(diffArea.startLine, diffArea.endLine + 1)
|
||||
.join('\n')
|
||||
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
|
||||
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
|
||||
this._diffAreasOfDocument[docUriStr].splice(index, 1)
|
||||
}
|
||||
|
||||
// refresh the diff area
|
||||
this.refreshDiffAreas(docUri)
|
||||
}
|
||||
}
|
||||
@@ -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++) {
|
||||
@@ -14,21 +14,54 @@ function getNonce() {
|
||||
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||
public static readonly viewId = 'void.viewnumberone';
|
||||
|
||||
public webview: Promise<vscode.Webview> // used to send messages to the webview
|
||||
|
||||
private readonly _extensionUri: vscode.Uri
|
||||
public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
|
||||
private _res: (c: vscode.Webview) => void // used to resolve the webview
|
||||
|
||||
private readonly _extensionUri: vscode.Uri
|
||||
|
||||
// private _webviewView?: vscode.WebviewView;
|
||||
private _webviewDeps: string[] = [];
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later, not sure for what though... was included in webviewProvider code
|
||||
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
|
||||
this._extensionUri = context.extensionUri
|
||||
|
||||
let temp_res: typeof this._res | undefined = undefined
|
||||
this.webview = new Promise((res, rej) => { temp_res = res })
|
||||
if (!temp_res) throw new Error("sidebar provider: resolver was undefined")
|
||||
if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined")
|
||||
this._res = temp_res
|
||||
}
|
||||
|
||||
// called by us
|
||||
updateWebviewHTML(webview: vscode.Webview) {
|
||||
this._webviewDeps = []
|
||||
|
||||
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 = generateNonce();
|
||||
|
||||
const webviewHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom View</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="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>`;
|
||||
|
||||
webview.html = webviewHTML;
|
||||
}
|
||||
|
||||
|
||||
// called internally by vscode
|
||||
resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
@@ -36,43 +69,17 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||
token: vscode.CancellationToken,
|
||||
) {
|
||||
|
||||
const webview = webviewView.webview
|
||||
const webview = webviewView.webview;
|
||||
|
||||
webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [this._extensionUri]
|
||||
};
|
||||
|
||||
// This allows us to use React in vscode
|
||||
// when you run `npm run build`, we take the React code in the `sidebar` folder
|
||||
// and compile it into `dist/sidebar/index.js` and `dist/sidebar/styles.css`
|
||||
// we render that code here
|
||||
const rootPath = this._extensionUri;
|
||||
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/index.js'));
|
||||
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/styles.css'));
|
||||
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath));
|
||||
|
||||
const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure
|
||||
|
||||
|
||||
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com']
|
||||
webview.html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom View</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ${allowed_urls.join(' ')}; img-src vscode-resource: https:; script-src 'nonce-${nonce}';style-src vscode-resource: 'unsafe-inline' http: https: data:;">
|
||||
<base href="${rootUri}/">
|
||||
<link href="${stylesUri}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
this.updateWebviewHTML(webview);
|
||||
|
||||
// resolve webview and _webviewView
|
||||
this._res(webview);
|
||||
// this._webviewView = webviewView;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { Ollama } from 'ollama/browser'
|
||||
import { VoidConfig } from '../sidebar/contextForConfig';
|
||||
|
||||
// import ollama from 'ollama'
|
||||
|
||||
export type ApiConfig = {
|
||||
anthropic: {
|
||||
apikey: string,
|
||||
},
|
||||
openai: {
|
||||
apikey: string
|
||||
},
|
||||
greptile: {
|
||||
apikey: string,
|
||||
githubPAT: string,
|
||||
repoinfo: {
|
||||
remote: string, // e.g. 'github'
|
||||
repository: string, // e.g. 'voideditor/void'
|
||||
branch: string // e.g. 'main'
|
||||
}
|
||||
},
|
||||
ollama: {
|
||||
// TODO
|
||||
},
|
||||
whichApi: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +17,8 @@ type SendLLMMessageFnTypeInternal = (params: {
|
||||
messages: LLMMessage[],
|
||||
onText: OnText,
|
||||
onFinalMessage: (input: string) => void,
|
||||
apiConfig: ApiConfig,
|
||||
onError: (error: string) => void,
|
||||
voidConfig: VoidConfig,
|
||||
})
|
||||
=> {
|
||||
abort: () => void
|
||||
@@ -48,7 +28,8 @@ type SendLLMMessageFnTypeExternal = (params: {
|
||||
messages: LLMMessage[],
|
||||
onText: OnText,
|
||||
onFinalMessage: (input: string) => void,
|
||||
apiConfig: ApiConfig | null,
|
||||
onError: (error: string) => void,
|
||||
voidConfig: VoidConfig | null,
|
||||
})
|
||||
=> {
|
||||
abort: () => void
|
||||
@@ -57,15 +38,14 @@ type SendLLMMessageFnTypeExternal = (params: {
|
||||
|
||||
|
||||
|
||||
// Claude
|
||||
const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
// Anthropic
|
||||
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
max_tokens: 1024,
|
||||
model: voidConfig.anthropic.model,
|
||||
max_tokens: parseInt(voidConfig.anthropic.maxTokens),
|
||||
messages: messages,
|
||||
});
|
||||
|
||||
@@ -85,10 +65,19 @@ 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
|
||||
// stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
|
||||
did_abort = true
|
||||
}
|
||||
|
||||
@@ -99,45 +88,116 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||
|
||||
|
||||
|
||||
// OpenAI
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
|
||||
let did_abort = false
|
||||
let didAbort = false
|
||||
let fullText = ''
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
let abort: () => void = () => { did_abort = true }
|
||||
let abort: () => void = () => {
|
||||
didAbort = true;
|
||||
};
|
||||
|
||||
const openai = new OpenAI({ apiKey: apiConfig.openai.apikey, dangerouslyAllowBrowser: true });
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
openai.chat.completions.create({
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
if (voidConfig.default.whichApi === 'openAI') {
|
||||
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
|
||||
options = { model: voidConfig.openAI.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, }
|
||||
}
|
||||
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, }
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
|
||||
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
abort = () => {
|
||||
// response.controller.abort()
|
||||
didAbort = true;
|
||||
}
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
if (didAbort) return;
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText(newText, 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 };
|
||||
};
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ""
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
let abort = () => {
|
||||
didAbort = true;
|
||||
};
|
||||
|
||||
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: voidConfig.ollama.model,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
})
|
||||
.then(async response => {
|
||||
.then(async stream => {
|
||||
abort = () => {
|
||||
// response.controller.abort() // this isn't needed now, to keep consistency with claude will leave it commented
|
||||
did_abort = true;
|
||||
// ollama.abort()
|
||||
didAbort = true
|
||||
}
|
||||
// when receive text
|
||||
try {
|
||||
for await (const chunk of response) {
|
||||
if (did_abort) return;
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
if (didAbort) return;
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
// when error/fail
|
||||
catch (error) {
|
||||
console.error('Error in OpenAI stream:', error);
|
||||
onFinalMessage(fullText);
|
||||
}
|
||||
// when we get the final message on this stream
|
||||
onFinalMessage(fullText)
|
||||
onFinalMessage(fullText);
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch(error => {
|
||||
onError(error)
|
||||
})
|
||||
|
||||
return { abort };
|
||||
};
|
||||
|
||||
@@ -147,26 +207,26 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||
// 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 }) => {
|
||||
|
||||
let did_abort = false
|
||||
let didAbort = false
|
||||
let fullText = ''
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
let abort: () => void = () => { did_abort = true }
|
||||
let abort: () => void = () => { didAbort = true }
|
||||
|
||||
|
||||
fetch('https://api.greptile.com/v2/query', {
|
||||
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
|
||||
@@ -177,7 +237,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
|
||||
})
|
||||
// TODO make this actually stream, right now it just sends one message at the end
|
||||
.then(async responseArr => {
|
||||
if (did_abort)
|
||||
if (didAbort)
|
||||
return
|
||||
|
||||
for (let response of responseArr) {
|
||||
@@ -205,81 +265,31 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
|
||||
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('Error in Greptile stream:', e);
|
||||
onFinalMessage(fullText);
|
||||
|
||||
onError(e)
|
||||
});
|
||||
|
||||
return { abort }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
if (!apiConfig) return { abort: () => { } }
|
||||
|
||||
const whichApi = apiConfig.whichApi
|
||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
if (!voidConfig) return { abort: () => { } }
|
||||
|
||||
if (whichApi === 'anthropic') {
|
||||
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig })
|
||||
switch (voidConfig.default.whichApi) {
|
||||
case 'anthropic':
|
||||
return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig });
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig });
|
||||
case 'ollama':
|
||||
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig });
|
||||
case 'greptile':
|
||||
return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig });
|
||||
default:
|
||||
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
return { abort: () => { } }
|
||||
}
|
||||
else if (whichApi === 'openai') {
|
||||
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig })
|
||||
}
|
||||
else if (whichApi === 'greptile') {
|
||||
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig })
|
||||
}
|
||||
else if (whichApi === 'ollama') {
|
||||
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO
|
||||
}
|
||||
else {
|
||||
console.error(`Error: whichApi was ${whichApi}, which is not recognized!`)
|
||||
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Ollama
|
||||
// const sendOllamaMsg: sendMsgFnType = ({ messages, onText, onFinalMessage }) => {
|
||||
|
||||
// let did_abort = false
|
||||
// let fullText = ''
|
||||
|
||||
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
// let abort: () => void = () => {
|
||||
// did_abort = true
|
||||
// }
|
||||
|
||||
// ollama.chat({ model: 'llama3.1', messages: messages, stream: true })
|
||||
// .then(async response => {
|
||||
|
||||
// abort = () => {
|
||||
// // response.abort() // this isn't needed now, to keep consistency with claude will leave it commented for now
|
||||
// did_abort = true;
|
||||
// }
|
||||
|
||||
// // when receive text
|
||||
// try {
|
||||
// for await (const part of response) {
|
||||
// if (did_abort) return
|
||||
// let newText = part.message.content
|
||||
// fullText += newText
|
||||
// onText(newText, fullText)
|
||||
// }
|
||||
// }
|
||||
// // when error/fail
|
||||
// catch (e) {
|
||||
// onFinalMessage(fullText)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // when we get the final message on this stream
|
||||
// onFinalMessage(fullText)
|
||||
// })
|
||||
|
||||
// return { abort };
|
||||
// };
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { PartialVoidConfig } from '../sidebar/contextForConfig';
|
||||
|
||||
|
||||
|
||||
// a selection is a frozen snapshot
|
||||
type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
|
||||
|
||||
type File = { filepath: vscode.Uri, content: string }
|
||||
|
||||
// an area that is currently being diffed
|
||||
type BaseDiffArea = {
|
||||
// use `startLine` and `endLine` instead of `range` for mutibility
|
||||
// bounds are relative to the file, inclusive
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
originalStartLine: number,
|
||||
originalEndLine: number,
|
||||
originalCode: string, // the original chunk of code (not necessarily the whole file)
|
||||
// `newCode: string,` is not included because it is the code in the actual file, `document.text()[startline: endLine + 1]`
|
||||
}
|
||||
|
||||
type DiffArea = BaseDiffArea & { diffareaid: number }
|
||||
|
||||
// the return type of diff creator
|
||||
type BaseDiff = {
|
||||
code: string; // representation of the diff in text
|
||||
deletedRange: vscode.Range; // relative to the file, inclusive
|
||||
insertedRange: vscode.Range;
|
||||
deletedCode: string;
|
||||
insertedCode: string;
|
||||
}
|
||||
|
||||
// each diff on the user's screen
|
||||
type Diff = {
|
||||
diffid: number,
|
||||
lenses: vscode.CodeLens[],
|
||||
} & BaseDiff
|
||||
|
||||
// editor -> sidebar
|
||||
type MessageToSidebar = (
|
||||
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
|
||||
| { type: 'ctrl+k', selection: CodeSelection }
|
||||
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
|
||||
| { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
|
||||
| { type: 'allThreads', threads: ChatThreads }
|
||||
| { type: 'startNewThread' }
|
||||
| { 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 ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
}
|
||||
|
||||
type ChatMessage =
|
||||
| {
|
||||
role: "user";
|
||||
content: string; // content sent to the llm
|
||||
displayContent: string; // content displayed to user
|
||||
selection: CodeSelection | null; // the user's selection
|
||||
files: vscode.Uri[]; // the files sent in the message
|
||||
}
|
||||
| {
|
||||
role: "assistant";
|
||||
content: string; // content received from LLM
|
||||
displayContent: string; // content displayed to user (this is the same as content for now)
|
||||
}
|
||||
|
||||
export {
|
||||
BaseDiff, BaseDiffArea,
|
||||
Diff, DiffArea,
|
||||
CodeSelection,
|
||||
File,
|
||||
MessageFromSidebar,
|
||||
MessageToSidebar,
|
||||
ChatThreads,
|
||||
ChatMessage,
|
||||
}
|
||||
@@ -1,99 +1,128 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { WebviewMessage } from './shared_types';
|
||||
import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider';
|
||||
import { getDiffedLines } from './getDiffedLines';
|
||||
import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider';
|
||||
import { DisplayChangesProvider } from './DisplayChangesProvider';
|
||||
import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types';
|
||||
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
|
||||
import { ApiConfig } from './common/sendLLMMessage';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const readFileContentOfUri = async (uri: vscode.Uri) => {
|
||||
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8').replace(/\r\n/g, '\n'); // must remove windows \r or every line will appear different because of it
|
||||
// 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 getApiConfig = () => {
|
||||
const apiConfig: ApiConfig = {
|
||||
anthropic: { apikey: vscode.workspace.getConfiguration('void').get('anthropicApiKey') ?? '' },
|
||||
openai: { apikey: vscode.workspace.getConfiguration('void').get('openAIApiKey') ?? '' },
|
||||
greptile: {
|
||||
apikey: vscode.workspace.getConfiguration('void').get('greptileApiKey') ?? '',
|
||||
githubPAT: vscode.workspace.getConfiguration('void').get('githubPAT') ?? '',
|
||||
repoinfo: {
|
||||
remote: 'github',
|
||||
repository: 'TODO',
|
||||
branch: 'main'
|
||||
}
|
||||
},
|
||||
ollama: {
|
||||
// apikey: vscode.workspace.getConfiguration('void').get('ollamaSettings') ?? '',
|
||||
},
|
||||
whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? ''
|
||||
}
|
||||
return apiConfig
|
||||
|
||||
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 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
|
||||
|
||||
|
||||
// const inset = vscode.window.createWebviewTextEditorInset(editor, 10, 10, {})
|
||||
// inset.webview.html = `
|
||||
// <html>
|
||||
// <body style="pointer-events:none;">Hello World!</body>
|
||||
// </html>
|
||||
// `;
|
||||
|
||||
|
||||
// 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));
|
||||
})
|
||||
);
|
||||
|
||||
// 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 codelens above each change
|
||||
const approvalCodeLensProvider = new ApprovalCodeLensProvider();
|
||||
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', approvalCodeLensProvider));
|
||||
const displayChangesProvider = new DisplayChangesProvider();
|
||||
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider));
|
||||
|
||||
// 4. Add approve/reject commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.approveDiff', async (params) => {
|
||||
approvalCodeLensProvider.approveDiff(params)
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
|
||||
displayChangesProvider.acceptDiff(params)
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.discardDiff', async (params) => {
|
||||
approvalCodeLensProvider.discardDiff(params)
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
|
||||
displayChangesProvider.rejectDiff(params)
|
||||
}));
|
||||
|
||||
|
||||
// 5.
|
||||
webviewProvider.webview.then(
|
||||
// 5. Receive messages from sidebar
|
||||
sidebarWebviewProvider.webview.then(
|
||||
webview => {
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
|
||||
// top navigation bar commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
|
||||
webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
|
||||
}))
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
|
||||
webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
|
||||
}))
|
||||
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') {
|
||||
|
||||
@@ -103,30 +132,70 @@ 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 === 'applyCode') {
|
||||
}
|
||||
else if (m.type === 'applyChanges') {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
vscode.window.showInformationMessage('No active editor!')
|
||||
return
|
||||
}
|
||||
const oldContents = await readFileContentOfUri(editor.document.uri)
|
||||
const suggestedEdits = getDiffedLines(oldContents, m.code)
|
||||
await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits)
|
||||
|
||||
|
||||
// create an area to show diffs
|
||||
const diffArea: BaseDiffArea = {
|
||||
startLine: 0, // in ctrl+L the start and end lines are the full document
|
||||
endLine: editor.document.lineCount,
|
||||
originalStartLine: 0,
|
||||
originalEndLine: editor.document.lineCount,
|
||||
originalCode: await readFileContentOfUri(editor.document.uri),
|
||||
}
|
||||
displayChangesProvider.addDiffArea(editor.document.uri, diffArea)
|
||||
|
||||
|
||||
// write new code `m.code` to the document
|
||||
// TODO update like this:
|
||||
// this._weAreEditing = true
|
||||
// await vscode.workspace.applyEdit(workspaceEdit)
|
||||
// await vscode.workspace.save(docUri)
|
||||
// this._weAreEditing = false
|
||||
await editor.edit(editBuilder => {
|
||||
editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code);
|
||||
});
|
||||
|
||||
// rediff the changes based on the diffAreas
|
||||
displayChangesProvider.refreshDiffAreas(editor.document.uri)
|
||||
|
||||
}
|
||||
else if (m.type === 'getApiConfig') {
|
||||
|
||||
const apiConfig = getApiConfig()
|
||||
console.log('Api config:', apiConfig)
|
||||
|
||||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
||||
|
||||
else if (m.type === '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 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
// import { diffLines, Change } from 'diff';
|
||||
import { BaseDiff } from './common/shared_types';
|
||||
|
||||
import { diff_match_patch } from 'diff-match-patch';
|
||||
|
||||
|
||||
const diffLines = (text1: string, text2: string) => {
|
||||
var dmp = new diff_match_patch();
|
||||
var a = dmp.diff_linesToChars_(text1, text2);
|
||||
var lineText1 = a.chars1;
|
||||
var lineText2 = a.chars2;
|
||||
var lineArray = a.lineArray;
|
||||
var diffs = dmp.diff_main(lineText1, lineText2, false);
|
||||
dmp.diff_charsToLines_(diffs, lineArray);
|
||||
// dmp.diff_cleanupSemantic(diffs);
|
||||
return diffs;
|
||||
}
|
||||
|
||||
|
||||
// TODO use a better diff algorithm
|
||||
export const findDiffs = (oldText: string, newText: string): BaseDiff[] => {
|
||||
|
||||
const diffs = diffLines(oldText, newText);
|
||||
|
||||
const blocks: BaseDiff[] = [];
|
||||
let reprBlock: string[] = [];
|
||||
let deletedBlock: string[] = [];
|
||||
let insertedBlock: string[] = [];
|
||||
let insertedLine = 0;
|
||||
let deletedLine = 0;
|
||||
let insertedStart = 0;
|
||||
let deletedStart = 0;
|
||||
|
||||
diffs.forEach(([operation, text]) => {
|
||||
|
||||
const lines = text.split('\n');
|
||||
|
||||
switch (operation) {
|
||||
|
||||
// insertion
|
||||
case 1:
|
||||
if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
|
||||
if (insertedBlock.length === 0) insertedStart = insertedLine;
|
||||
insertedLine += lines.length - 1; // Update only the line count for new text
|
||||
insertedBlock.push(text);
|
||||
reprBlock.push(lines.map(line => `+ ${line}`).join('\n'));
|
||||
break;
|
||||
|
||||
// deletion
|
||||
case -1:
|
||||
if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
|
||||
if (deletedBlock.length === 0) deletedStart = deletedLine;
|
||||
deletedLine += lines.length - 1; // Update only the line count for old text
|
||||
deletedBlock.push(text);
|
||||
reprBlock.push(lines.map(line => `- ${line}`).join('\n'));
|
||||
break;
|
||||
|
||||
// no change
|
||||
case 0:
|
||||
// If we have a pending block, add it to the blocks array
|
||||
if (insertedBlock.length > 0 || deletedBlock.length > 0) {
|
||||
blocks.push({
|
||||
code: reprBlock.join(''),
|
||||
deletedCode: deletedBlock.join(''),
|
||||
insertedCode: insertedBlock.join(''),
|
||||
deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
|
||||
insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
|
||||
});
|
||||
}
|
||||
|
||||
// Reset the block variables
|
||||
reprBlock = [];
|
||||
deletedBlock = [];
|
||||
insertedBlock = [];
|
||||
|
||||
// Update line counts for unchanged text
|
||||
insertedLine += lines.length - 1;
|
||||
deletedLine += lines.length - 1;
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Add any remaining blocks after the loop ends
|
||||
if (insertedBlock.length > 0 || deletedBlock.length > 0) {
|
||||
blocks.push({
|
||||
code: reprBlock.join(''),
|
||||
deletedCode: deletedBlock.join(''),
|
||||
insertedCode: insertedBlock.join(''),
|
||||
deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
|
||||
insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
|
||||
});
|
||||
}
|
||||
|
||||
return blocks;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// export const findDiffs = (oldText: string, newText: string): DiffBlock[] => {
|
||||
|
||||
// const diffs = diffLines(oldText, newText);
|
||||
|
||||
// const blocks: DiffBlock[] = [];
|
||||
|
||||
// let reprBlock: string[] = [];
|
||||
// let deletedBlock: string[] = [];
|
||||
// let insertedBlock: string[] = [];
|
||||
|
||||
// let insertedEnd = 0;
|
||||
// let deletedEnd = 0;
|
||||
// let insertedStart = 0;
|
||||
// let deletedStart = 0;
|
||||
|
||||
// diffs.forEach(part => {
|
||||
|
||||
// part.count = part.count ?? 0
|
||||
|
||||
// // if the part is an addition or deletion, add it to the current block
|
||||
// if (part.added || part.removed) {
|
||||
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
|
||||
// if (part.added) {
|
||||
// if (insertedBlock.length === 0) insertedStart = insertedEnd;
|
||||
// insertedEnd += part.count
|
||||
// insertedBlock.push(part.value);
|
||||
// reprBlock.push(part.value.split('\n').map(line => `+ ${line}`).join('\n'));
|
||||
// }
|
||||
// if (part.removed) {
|
||||
// if (deletedBlock.length === 0) deletedStart = deletedEnd;
|
||||
// deletedEnd += part.count
|
||||
// deletedBlock.push(part.value);
|
||||
// reprBlock.push(part.value.split('\n').map(line => `- ${line}`).join('\n'));
|
||||
// }
|
||||
// }
|
||||
|
||||
// // if the part is unchanged, finalize the block and add it to the array
|
||||
// else {
|
||||
// // if the block is not null, add it to the array
|
||||
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
|
||||
// blocks.push({
|
||||
// code: reprBlock.join('\n'),
|
||||
// deletedCode: deletedBlock.join(''),
|
||||
// insertedCode: insertedBlock.join(''),
|
||||
// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER),
|
||||
// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER),
|
||||
// });
|
||||
// }
|
||||
|
||||
// // update block variables
|
||||
// reprBlock = [];
|
||||
// deletedBlock = [];
|
||||
// insertedBlock = [];
|
||||
// insertedEnd += part.count;
|
||||
// deletedEnd += part.count;
|
||||
|
||||
// }
|
||||
|
||||
// })
|
||||
|
||||
// // finally, add the last block to the array
|
||||
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
|
||||
// blocks.push({
|
||||
// code: reprBlock.join('\n'),
|
||||
// deletedCode: deletedBlock.join(''),
|
||||
// insertedCode: insertedBlock.join(''),
|
||||
// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER),
|
||||
// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER),
|
||||
// });
|
||||
// }
|
||||
|
||||
// return blocks;
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// import { diffLines, Change } from 'diff';
|
||||
|
||||
// export type SuggestedEdit = {
|
||||
// // start/end of current file
|
||||
// startLine: number;
|
||||
// endLine: number;
|
||||
|
||||
// // start/end of original file
|
||||
// originalStartLine: number,
|
||||
// originalEndLine: number,
|
||||
|
||||
// // original content (originalfile[originalStart...originalEnd])
|
||||
// originalContent: string;
|
||||
// newContent: string;
|
||||
// }
|
||||
|
||||
// export function getDiffedLines(oldStr: string, newStr: string) {
|
||||
// // an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
|
||||
// const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
|
||||
// console.debug('Line by line changes', lineByLineChanges)
|
||||
|
||||
// lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
|
||||
|
||||
// let oldFileLineNum: number = 0;
|
||||
// let newFileLineNum: number = 0;
|
||||
|
||||
// let streakStartInNewFile: number | undefined = undefined
|
||||
// let streakStartInOldFile: number | undefined = undefined
|
||||
|
||||
// let oldStrLines = oldStr.split('\n')
|
||||
// let newStrLines = newStr.split('\n')
|
||||
|
||||
// const replacements: SuggestedEdit[] = []
|
||||
|
||||
// for (let line of lineByLineChanges) {
|
||||
// // no change on this line
|
||||
// if (!line.added && !line.removed) {
|
||||
// // if we were on a streak, add it
|
||||
// if (streakStartInNewFile !== undefined) {
|
||||
|
||||
// const startLine = streakStartInNewFile
|
||||
// const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
// const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
|
||||
|
||||
// const originalStartLine = streakStartInOldFile!
|
||||
// const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
// const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
|
||||
|
||||
// const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent }
|
||||
|
||||
// replacements.push(replacement)
|
||||
// streakStartInNewFile = undefined
|
||||
// streakStartInOldFile = undefined
|
||||
// }
|
||||
|
||||
// oldFileLineNum += line.count ?? 0;
|
||||
// newFileLineNum += line.count ?? 0;
|
||||
// }
|
||||
|
||||
|
||||
// // line was removed from old file
|
||||
// else if (line.removed) {
|
||||
|
||||
// // if we weren't on a streak, start one on this current line num
|
||||
// if (streakStartInNewFile === undefined) {
|
||||
// streakStartInNewFile = newFileLineNum
|
||||
// streakStartInOldFile = oldFileLineNum
|
||||
// }
|
||||
|
||||
// oldFileLineNum += line.count ?? 0 // we processed the line so add 1
|
||||
// }
|
||||
|
||||
// // line was added to new file
|
||||
// else if (line.added) {
|
||||
|
||||
// // if we weren't on a streak, start one on this current line num
|
||||
// if (streakStartInNewFile === undefined) {
|
||||
// streakStartInNewFile = newFileLineNum
|
||||
// streakStartInOldFile = oldFileLineNum
|
||||
// }
|
||||
|
||||
// newFileLineNum += line.count ?? 0; // we processed the line so add 1
|
||||
// }
|
||||
|
||||
// } // end for
|
||||
|
||||
// console.debug('Replacements', replacements)
|
||||
|
||||
// return replacements
|
||||
|
||||
// }
|
||||
@@ -1,91 +0,0 @@
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
export type SuggestedEdit = {
|
||||
// start/end of current file
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
|
||||
// start/end of original file
|
||||
originalStartLine: number,
|
||||
originalEndLine: number,
|
||||
|
||||
// original content (originalfile[originalStart...originalEnd])
|
||||
originalContent: string;
|
||||
newContent: string;
|
||||
}
|
||||
|
||||
export function getDiffedLines(oldStr: string, newStr: string) {
|
||||
// an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
|
||||
const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
|
||||
console.debug('Line by line changes', lineByLineChanges)
|
||||
|
||||
lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
|
||||
|
||||
let oldFileLineNum: number = 0;
|
||||
let newFileLineNum: number = 0;
|
||||
|
||||
let streakStartInNewFile: number | undefined = undefined
|
||||
let streakStartInOldFile: number | undefined = undefined
|
||||
|
||||
let oldStrLines = oldStr.split('\n')
|
||||
let newStrLines = newStr.split('\n')
|
||||
|
||||
const replacements: SuggestedEdit[] = []
|
||||
|
||||
for (let line of lineByLineChanges) {
|
||||
// no change on this line
|
||||
if (!line.added && !line.removed) {
|
||||
// if we were on a streak, add it
|
||||
if (streakStartInNewFile !== undefined) {
|
||||
|
||||
const startLine = streakStartInNewFile
|
||||
const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
|
||||
|
||||
const originalStartLine = streakStartInOldFile!
|
||||
const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
|
||||
|
||||
const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent }
|
||||
|
||||
replacements.push(replacement)
|
||||
streakStartInNewFile = undefined
|
||||
streakStartInOldFile = undefined
|
||||
}
|
||||
|
||||
oldFileLineNum += line.count ?? 0;
|
||||
newFileLineNum += line.count ?? 0;
|
||||
}
|
||||
|
||||
|
||||
// line was removed from old file
|
||||
else if (line.removed) {
|
||||
|
||||
// if we weren't on a streak, start one on this current line num
|
||||
if (streakStartInNewFile === undefined) {
|
||||
streakStartInNewFile = newFileLineNum
|
||||
streakStartInOldFile = oldFileLineNum
|
||||
}
|
||||
|
||||
oldFileLineNum += line.count ?? 0 // we processed the line so add 1
|
||||
}
|
||||
|
||||
// line was added to new file
|
||||
else if (line.added) {
|
||||
|
||||
// if we weren't on a streak, start one on this current line num
|
||||
if (streakStartInNewFile === undefined) {
|
||||
streakStartInNewFile = newFileLineNum
|
||||
streakStartInOldFile = oldFileLineNum
|
||||
}
|
||||
|
||||
newFileLineNum += line.count ?? 0; // we processed the line so add 1
|
||||
}
|
||||
|
||||
} // end for
|
||||
|
||||
console.debug('Replacements', replacements)
|
||||
|
||||
return replacements
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ApiConfig } from './common/sendLLMMessage';
|
||||
|
||||
// a selection is a frozen snapshot
|
||||
type Selection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
|
||||
|
||||
type File = { filepath: vscode.Uri, content: string }
|
||||
|
||||
type WebviewMessage = (
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'ctrl+l', selection: Selection } // user presses ctrl+l in the editor
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'applyCode', code: string } // user clicks "apply" in the sidebar
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'requestFiles', filepaths: vscode.Uri[] }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'getApiConfig' }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'apiConfig', apiConfig: ApiConfig }
|
||||
|
||||
)
|
||||
|
||||
type Command = WebviewMessage['type']
|
||||
|
||||
export {
|
||||
Selection,
|
||||
File,
|
||||
WebviewMessage,
|
||||
Command,
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
};
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import React, { JSX, useState } from 'react';
|
||||
import { MarkedToken, Token, TokensList } from 'marked';
|
||||
import { awaitVSCodeResponse, getVSCodeAPI } from './getVscodeApi';
|
||||
|
||||
|
||||
// code block with Apply button at top
|
||||
export const BlockCode = ({ text, disableApplyButton = false }: { text: string, disableApplyButton?: boolean }) => {
|
||||
return <div className='py-1'>
|
||||
{disableApplyButton ? null : <div className='text-sm'>
|
||||
<button className='btn btn-secondary px-3 py-1 text-sm rounded-t-sm'
|
||||
onClick={async () => { getVSCodeAPI().postMessage({ type: 'applyCode', code: text }) }}>Apply</button>
|
||||
</div>}
|
||||
<div className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${disableApplyButton ? '' : 'rounded-tl-none'}`}>
|
||||
<pre className='p-3'>
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const Render = ({ token }: { token: Token }) => {
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
|
||||
if (t.type === "space") {
|
||||
return <span>{t.raw}</span>;
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
return <BlockCode text={t.text} />
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements;
|
||||
return <HeadingTag>{t.text}</HeadingTag>;
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index} style={{ textAlign: t.align[index] || 'left' }}>
|
||||
{cell.raw}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td key={cellIndex} style={{ textAlign: t.align[cellIndex] || 'left' }}>
|
||||
{cell.raw}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
return <hr />;
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
return <blockquote>{t.text}</blockquote>;
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
|
||||
const ListTag = t.ordered ? 'ol' : 'ul';
|
||||
return (
|
||||
<ListTag start={t.start !== '' ? t.start : undefined}
|
||||
className={`list-inside ${t.ordered ? 'list-decimal' : 'list-disc'}`}
|
||||
>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
{item.text}
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
);
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
return <p>
|
||||
{t.tokens.map((token, index) => (
|
||||
<Render key={index} token={token} />
|
||||
))}
|
||||
</p>;
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
return <pre>{`<html>`}{t.raw}{`</html>`}</pre>;
|
||||
}
|
||||
|
||||
if (t.type === "text" || t.type === "escape") {
|
||||
return <span>{t.raw}</span>;
|
||||
}
|
||||
|
||||
if (t.type === "def") {
|
||||
return null; // Definitions are typically not rendered
|
||||
}
|
||||
|
||||
if (t.type === "link") {
|
||||
return <a href={t.href} title={t.title ?? undefined}>{t.text}</a>;
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />;
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
return <strong>{t.text}</strong>;
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
return <em>{t.text}</em>;
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return <code className='text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono'>{t.text}</code>;
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
return <br />;
|
||||
}
|
||||
|
||||
if (t.type === "del") {
|
||||
return <del>{t.text}</del>;
|
||||
}
|
||||
|
||||
|
||||
// default
|
||||
return <div className='bg-orange-50 rounded-sm overflow-hidden'>
|
||||
<span className='text-xs text-orange-500'>Unknown type:</span>
|
||||
{t.raw}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const MarkdownRender = ({ tokens }: { tokens: TokensList }) => {
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<Render key={index} token={token} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownRender;
|
||||
@@ -1,320 +1,64 @@
|
||||
import React, { useState, ChangeEvent, useEffect, useRef, useCallback, FormEvent } from "react"
|
||||
import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage"
|
||||
import { Command, File, Selection, WebviewMessage } from "../shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
|
||||
|
||||
import { marked } from 'marked';
|
||||
import MarkdownRender, { BlockCode } from "./MarkdownRender";
|
||||
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
|
||||
const filesStr = (fullFiles: File[]) => {
|
||||
return fullFiles.map(({ filepath, content }) =>
|
||||
`
|
||||
${filepath.fsPath}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\``).join('\n')
|
||||
}
|
||||
|
||||
const userInstructionsStr = (instructions: string, files: File[], selection: Selection | null) => {
|
||||
return `
|
||||
${filesStr(files)}
|
||||
|
||||
${!selection ? '' : `
|
||||
I am currently selecting this code:
|
||||
\`\`\`${selection.selectionStr}\`\`\`
|
||||
`}
|
||||
|
||||
Please edit the code following these instructions:
|
||||
${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 FilesSelector = ({ files, setFiles }: { files: vscode.Uri[], setFiles: (files: vscode.Uri[]) => void }) => {
|
||||
return files.length !== 0 && <div className='my-2'>
|
||||
Include files:
|
||||
{files.map((filename, i) =>
|
||||
<div key={i} className='flex'>
|
||||
{/* X button on a file */}
|
||||
<button type='button' onClick={() => {
|
||||
let file_index = files.indexOf(filename)
|
||||
setFiles([...files.slice(0, file_index), ...files.slice(file_index + 1, Infinity)])
|
||||
}}>
|
||||
-{' '}<span className='text-gray-500'>{getBasename(filename.fsPath)}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => {
|
||||
return files.length !== 0 && <div className='text-xs my-2'>
|
||||
{files.map((filename, i) =>
|
||||
<div key={i} className='flex'>
|
||||
<button type='button'
|
||||
className='btn btn-secondary pointer-events-none'
|
||||
onClick={() => {
|
||||
// TODO redirect to the document filename.fsPath, when add this remove pointer-events-none
|
||||
}}>
|
||||
-{' '}<span className='text-gray-100'>{getBasename(filename.fsPath)}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
const children = chatMessage.displayContent
|
||||
|
||||
if (!children)
|
||||
return null
|
||||
|
||||
let chatbubbleContents: React.ReactNode
|
||||
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<IncludedFiles files={chatMessage.files} />
|
||||
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} disableApplyButton={true} />}
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer
|
||||
chatbubbleContents = <MarkdownRender tokens={tokens} /> // 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 getBasename = (pathStr: string) => {
|
||||
// "unixify" path
|
||||
pathStr = pathStr.replace(/[/\\]+/g, '/'); // replace any / or \ or \\ with /
|
||||
const parts = pathStr.split('/') // split on /
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
type ChatMessage = {
|
||||
role: 'user'
|
||||
content: string, // content sent to the llm
|
||||
displayContent: string, // content displayed to user
|
||||
selection: Selection | null, // the user's selection
|
||||
files: vscode.Uri[], // the files sent in the message
|
||||
} | {
|
||||
role: 'assistant',
|
||||
content: string, // content received from LLM
|
||||
displayContent: string // content displayed to user (this is the same as content for now)
|
||||
}
|
||||
|
||||
|
||||
// const [stateRef, setState] = useInstantState(initVal)
|
||||
// setState instantly changes the value of stateRef instead of having to wait until the next render
|
||||
const useInstantState = <T,>(initVal: T) => {
|
||||
const stateRef = useRef<T>(initVal)
|
||||
const [_, setS] = useState<T>(initVal)
|
||||
const setState = useCallback((newVal: T) => {
|
||||
setS(newVal);
|
||||
stateRef.current = newVal;
|
||||
}, [])
|
||||
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
|
||||
}
|
||||
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
|
||||
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 = () => {
|
||||
|
||||
// state of current message
|
||||
const [selection, setSelection] = useState<Selection | 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 [chatMessageHistory, setChatHistory] = useState<ChatMessage[]>([])
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [isLoading, setIsLoading] = 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 file if it's not a duplicate
|
||||
if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, filepath])
|
||||
|
||||
}
|
||||
// when get apiConfig, set
|
||||
else if (m.type === 'apiConfig') {
|
||||
setApiConfig(m.apiConfig)
|
||||
}
|
||||
|
||||
}
|
||||
window.addEventListener('message', listener);
|
||||
return () => { window.removeEventListener('message', listener) }
|
||||
}, [files, selection])
|
||||
|
||||
|
||||
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 }
|
||||
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||
|
||||
// send message to claude
|
||||
let { abort } = sendLLMMessage({
|
||||
messages: [...chatMessageHistory.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
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
||||
setChatHistory(chatMessageHistory => [...chatMessageHistory, 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 }
|
||||
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
}, [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">
|
||||
<div className="overflow-y-auto overflow-x-hidden space-y-4">
|
||||
{/* previous messages */}
|
||||
{chatMessageHistory.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">
|
||||
{/* selected files */}
|
||||
<FilesSelector files={files} setFiles={setFiles} />
|
||||
{/* selected code */}
|
||||
{!selection?.selectionStr ? null
|
||||
: (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="absolute top-2 right-2 text-white hover:text-gray-300 z-10"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
<BlockCode text={selection.selectionStr} disableApplyButton={true} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-row items-center rounded-md p-2 input"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||
<div className={`flex flex-col h-screen w-full`}>
|
||||
|
||||
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 className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
|
||||
<SidebarThreadSelector onClose={() => setTab('chat')} />
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'chat' && tab !== 'threadSelector' ? 'hidden' : ''}`}>
|
||||
<SidebarChat chatInputRef={chatInputRef} />
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
|
||||
<SidebarSettings />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
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 { 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 { allThreads, currentThread, 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 = currentThread?.messages
|
||||
|
||||
captureEvent(eventId, {
|
||||
whichApi: whichApi,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent.length })),
|
||||
version: '2024-10-19',
|
||||
...extras,
|
||||
})
|
||||
}, [currentThread?.messages, voidConfig.default])
|
||||
|
||||
|
||||
// if they pressed the + to add a new chat
|
||||
useOnVSCodeMessage('startNewThread', (m) => {
|
||||
// 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 message to chat history
|
||||
const userContent = userInstructionsStr(instructions, relevantFiles.files, selection)
|
||||
// console.log('prompt:\n', content)
|
||||
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
|
||||
// send message to LLM
|
||||
|
||||
captureChatEvent('Chat - Sending Message', { messageLength: instructions.length })
|
||||
const submit_time = new Date()
|
||||
|
||||
let { abort } = sendLLMMessage({
|
||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userContent }],
|
||||
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)
|
||||
},
|
||||
voidConfig: voidConfig
|
||||
})
|
||||
abortFnRef.current = abort
|
||||
|
||||
}
|
||||
|
||||
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 */}
|
||||
{currentThread !== null && currentThread.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,101 @@
|
||||
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'
|
||||
/>
|
||||
</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,77 @@
|
||||
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 { allThreads, currentThread, switchToThread } = useThreads()
|
||||
|
||||
// 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 === currentThread?.id ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
{btnString}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
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,
|
||||
),
|
||||
},
|
||||
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,
|
||||
),
|
||||
|
||||
maxTokens: configEnum(
|
||||
"Anthropic max number of tokens to output.",
|
||||
'8192',
|
||||
[
|
||||
"1024",
|
||||
"2048",
|
||||
"4096",
|
||||
"8192"
|
||||
] 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']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
|
||||
import { ChatMessage, ChatThreads } from "../common/shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
|
||||
|
||||
|
||||
// a "thread" means a chat message history
|
||||
type ConfigForThreadsValueType = {
|
||||
readonly allThreads: ChatThreads | null,
|
||||
readonly currentThread: ChatThreads[string] | null;
|
||||
addMessageToHistory: (message: ChatMessage) => void;
|
||||
switchToThread: (threadId: string) => void;
|
||||
startNewThread: () => void;
|
||||
}
|
||||
|
||||
const ThreadsContext = createContext<ConfigForThreadsValueType>(undefined as unknown as ConfigForThreadsValueType)
|
||||
|
||||
const createNewThread = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// const [stateRef, setState] = useInstantState(initVal)
|
||||
// setState instantly changes the value of stateRef instead of having to wait until the next render
|
||||
const useInstantState = <T,>(initVal: T) => {
|
||||
const stateRef = useRef<T>(initVal)
|
||||
const [_, setS] = useState<T>(initVal)
|
||||
const setState = useCallback((newVal: T) => {
|
||||
setS(newVal);
|
||||
stateRef.current = newVal;
|
||||
}, [])
|
||||
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
|
||||
}
|
||||
|
||||
|
||||
export function ThreadsProvider({ children }: { children: ReactNode }) {
|
||||
const [allThreads, setAllThreads] = useInstantState<ChatThreads>({})
|
||||
const [currentThreadId, setCurrentThreadId] = useInstantState<string | null>(null)
|
||||
|
||||
// this loads allThreads in on mount
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: 'getAllThreads' })
|
||||
awaitVSCodeResponse('allThreads')
|
||||
.then(response => {
|
||||
setAllThreads(response.threads)
|
||||
})
|
||||
}, [setAllThreads])
|
||||
|
||||
|
||||
return (
|
||||
<ThreadsContext.Provider
|
||||
value={{
|
||||
allThreads: allThreads.current,
|
||||
currentThread: currentThreadId.current === null || allThreads.current === null ? null : allThreads.current[currentThreadId.current],
|
||||
addMessageToHistory: (message: ChatMessage) => {
|
||||
let currentThread: ChatThreads[string]
|
||||
if (!(currentThreadId.current === null || allThreads.current === null)) {
|
||||
currentThread = allThreads.current[currentThreadId.current]
|
||||
}
|
||||
else {
|
||||
currentThread = createNewThread()
|
||||
setCurrentThreadId(currentThread.id)
|
||||
}
|
||||
|
||||
setAllThreads({
|
||||
...allThreads.current,
|
||||
[currentThread.id]: {
|
||||
...currentThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...currentThread.messages, message],
|
||||
}
|
||||
})
|
||||
|
||||
getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread })
|
||||
},
|
||||
switchToThread: (threadId: string) => {
|
||||
setCurrentThreadId(threadId);
|
||||
},
|
||||
startNewThread: () => {
|
||||
const newThread = createNewThread()
|
||||
setAllThreads({
|
||||
...allThreads.current,
|
||||
[newThread.id]: newThread
|
||||
})
|
||||
setCurrentThreadId(newThread.id)
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThreadsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useThreads(): ConfigForThreadsValueType {
|
||||
const context = useContext<ConfigForThreadsValueType>(ThreadsContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useThreads missing Provider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -1,43 +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": [],
|
||||
"applyCode": [],
|
||||
"requestFiles": [],
|
||||
"ctrl+k": [],
|
||||
"files": [],
|
||||
"apiConfig": [],
|
||||
"getApiConfig": []
|
||||
"partialVoidConfig": [],
|
||||
"startNewThread": [],
|
||||
"allThreads": [],
|
||||
"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;
|
||||
|
||||
@@ -1,16 +1,66 @@
|
||||
import * as React from 'react'
|
||||
import * as ReactDOM from 'react-dom/client'
|
||||
import Sidebar from './Sidebar'
|
||||
import * as React from "react"
|
||||
import { useEffect } from "react"
|
||||
import * as ReactDOM from "react-dom/client"
|
||||
import Sidebar from "./Sidebar"
|
||||
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')
|
||||
if (typeof document === "undefined") {
|
||||
console.log("index.tsx error: document was undefined")
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById('root')!
|
||||
console.log('root Element', rootElement)
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(<Sidebar />)
|
||||
|
||||
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)
|
||||
})();
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
|
||||
|
||||
const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
|
||||
const customStyle = {
|
||||
...atomOneDarkReasonable,
|
||||
'code[class*="language-"]': {
|
||||
...atomOneDarkReasonable['code[class*="language-"]'],
|
||||
background: "none",
|
||||
},
|
||||
}
|
||||
|
||||
return (<>
|
||||
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
|
||||
|
||||
{!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`}
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
language={language ?? 'plaintext'} // TODO must auto detect language
|
||||
style={customStyle}
|
||||
className={"rounded-sm"}
|
||||
>
|
||||
{text}
|
||||
</SyntaxHighlighter>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlockCode
|
||||
@@ -0,0 +1,223 @@
|
||||
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 => {
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
|
||||
if (t.type === "space") {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
return <BlockCode
|
||||
text={t.text}
|
||||
language={t.lang}
|
||||
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
|
||||
return <HeadingTag>{t.text}</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
|
||||
{cell.raw}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td
|
||||
key={cellIndex}
|
||||
style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
>
|
||||
{cell.raw}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
return <hr />
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
return <blockquote>{t.text}</blockquote>
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
const ListTag = t.ordered ? "ol" : "ul"
|
||||
return (
|
||||
<ListTag
|
||||
start={t.start ? t.start : undefined}
|
||||
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
<MarkdownRender string={item.text} nested={true} />
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} />
|
||||
))}
|
||||
</>
|
||||
if (nested)
|
||||
return contents
|
||||
return <p>{contents}</p>
|
||||
}
|
||||
|
||||
// don't actually render <html> tags, just render strings of them
|
||||
if (t.type === "html") {
|
||||
return (
|
||||
<pre>
|
||||
{`<html>`}
|
||||
{t.raw}
|
||||
{`</html>`}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "text" || t.type === "escape") {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "def") {
|
||||
return <></> // Definitions are typically not rendered
|
||||
}
|
||||
|
||||
if (t.type === "link") {
|
||||
return (
|
||||
<a href={t.href} title={t.title ?? undefined}>
|
||||
{t.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
return <strong>{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
return <em>{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return (
|
||||
<code className="text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono">
|
||||
{t.text}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
return <br />
|
||||
}
|
||||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
return <del>{t.text}</del>
|
||||
}
|
||||
|
||||
// default
|
||||
return (
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden">
|
||||
<span className="text-xs text-orange-500">Unknown type:</span>
|
||||
{t.raw}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} nested={nested} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownRender
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -3,33 +3,42 @@
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
font-size: var(--vscode-font-size);
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply cursor-pointer transition-colors;
|
||||
@apply cursor-pointer transition-colors;
|
||||
|
||||
&.btn-primary {
|
||||
@apply bg-vscode-button-bg text-vscode-button-fg;
|
||||
&.btn-primary {
|
||||
@apply bg-vscode-button-bg text-vscode-button-fg;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-hoverBg;
|
||||
}
|
||||
}
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-hoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
|
||||
&.btn-sm {
|
||||
@apply px-3 py-1 text-sm;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-secondary-hoverBg;
|
||||
}
|
||||
}
|
||||
&.btn-secondary {
|
||||
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-75 cursor-not-allowed;
|
||||
}
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-secondary-hoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -7,17 +7,22 @@ module.exports = {
|
||||
extend: {
|
||||
colors: {
|
||||
vscode: {
|
||||
'editor-bg': "var(--vscode-editor-background)",
|
||||
'editor-fg': "var(--vscode-editor-foreground)",
|
||||
'input-bg': "var(--vscode-input-background)",
|
||||
'input-fg': "var(--vscode-input-foreground)",
|
||||
'input-border': "var(--vscode-input-border)",
|
||||
'button-fg': "var(--vscode-button-foreground)",
|
||||
'button-bg': "var(--vscode-button-background)",
|
||||
'button-hoverBg': "var(--vscode-button-hoverBackground)",
|
||||
'button-secondary-fg': "var(--vscode-button-secondaryForeground)",
|
||||
'button-secondary-bg': "var(--vscode-button-secondaryBackground)",
|
||||
'button-secondary-hoverBg': "var(--vscode-button-secondaryHoverBackground)",
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
"editor-bg": "var(--vscode-editor-background)",
|
||||
"editor-fg": "var(--vscode-editor-foreground)",
|
||||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"button-fg": "var(--vscode-button-foreground)",
|
||||
"button-bg": "var(--vscode-button-background)",
|
||||
"button-hoverBg": "var(--vscode-button-hoverBackground)",
|
||||
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
|
||||
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
|
||||
"button-secondary-hoverBg": "var(--vscode-button-secondaryHoverBackground)",
|
||||
"dropdown-bg": "var(--vscode-settings-dropdownBackground)",
|
||||
"dropdown-foreground": "var(--vscode-settings-dropdownForeground)",
|
||||
"dropdown-border": "var(--vscode-settings-dropdownBorder)",
|
||||
"focus-border": "var(--vscode-focusBorder)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Generated
+8
@@ -137,6 +137,7 @@
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opn": "^6.0.0",
|
||||
"original-fs": "^1.2.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"p-all": "^1.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
@@ -14203,6 +14204,13 @@
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/original-fs": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/original-fs/-/original-fs-1.2.0.tgz",
|
||||
"integrity": "sha512-IGo+qFumpIV65oDchJrqL0BOk9kr82fObnTesNJt8t3YgP6vfqcmRs0ofPzg3D9PKMeBHt7lrg1k/6L+oFdS8g==",
|
||||
"dev": true,
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/os-browserify": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opn": "^6.0.0",
|
||||
"original-fs": "^1.2.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"p-all": "^1.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
|
||||
Regular → Executable
@@ -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 {
|
||||
|
||||
Vendored
-1
@@ -9479,7 +9479,6 @@ declare module 'vscode' {
|
||||
*/
|
||||
readonly extensionHostPort: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content settings for a webview.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user