Compare commits
74 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 | |||
| ef94de48c2 | |||
| 14a8b0ee4e | |||
| c3e2dc1621 | |||
| bed7929e48 | |||
| b8a5e5b67c |
+46
-54
@@ -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,19 +40,19 @@ 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`). Also make sure you have Python on your system.
|
||||
|
||||
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.
|
||||
|
||||
@@ -60,64 +60,62 @@ Make sure you're on the correct NodeJS version as per `.nvmrc`.
|
||||
npm install
|
||||
```
|
||||
|
||||
2. In VS Code, press <kbd>Ctrl+Shift+B</kbd> to start the build process - this can take some time. If you're not using VS Code, run `npm run watch` instead.
|
||||
2. Run `npm run watch`.
|
||||
|
||||
3. Run `./scripts/code.sh` in your terminal.
|
||||
This can take ~5 min. It's done when you see something like:
|
||||
|
||||
```
|
||||
[watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms
|
||||
[watch-client ] [00:38:06] Finished compilation with 0 errors after 46248 ms
|
||||
[watch-client ] [00:38:07] Starting compilation...
|
||||
[watch-client ] [00:38:07] Finished compilation with 0 errors after 5 ms
|
||||
```
|
||||
|
||||
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
|
||||
|
||||
3. In a new terminal, run `./scripts/code.sh`.
|
||||
|
||||
This should open up the built IDE after loading for some time. To see new changes without restarting the build, use <kbd>Ctrl+Shift+P</kbd> and run "Reload Window".
|
||||
|
||||
To bundle the IDE, run `npm run gulp vscode-darwin-arm64`. Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin-arm64 | linux-ia32 | linux-x64 | linux-arm}(-min)`
|
||||
|
||||
If you're on Windows, we recommend running the project inside a dev container. VSCode should prompt you to do this automatically.
|
||||
|
||||
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
|
||||
|
||||
|
||||
**Common Fixes:**
|
||||
|
||||
- Make sure you have the same NodeJS version as `.nvmrc`.
|
||||
|
||||
|
||||
### 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.
|
||||
@@ -128,36 +126,30 @@ Please submit a pull request once you've made a change. Here are a few guideline
|
||||
|
||||
- A PR should be about one *single* feature change. The fewer items you change, the more likely the PR is to be accepted.
|
||||
|
||||
- Your PR should contain a description that first explains at a high level what you did, and then describes the exact changes you made (and to which files). Please don't use vague statements like "refactored code" or "improved types" (instead, describe what code you refactored, or what types you changed).
|
||||
- Your PR should contain a description that first explains at a high level what you did, and then describes the exact changes you made (and to which files). Please don't use vague statements like "refactored code" or "improved types" (instead, describe what code you refactored, or what types you changed).
|
||||
|
||||
- Your title should clearly describe the change you made.
|
||||
|
||||
- Add tags to help us stay organized!
|
||||
|
||||
- Please don't open a new Issue for your PR. Just submit the PR.
|
||||
|
||||
- Avoid refactoring and making feature changes in the same PR.
|
||||
|
||||
- Write good code. For example, a common mistake when people edit Void's config is to hard-code a default value like `'claude-3.5'` in 2+ separate places. Please follow best practices or describe your thought process if you had to compromise.
|
||||
- Try to avoid refactoring and making feature changes in the same PR.
|
||||
|
||||
# Relevant files
|
||||
|
||||
We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
|
||||
|
||||
- README.md
|
||||
- CONTRIBUTING.md
|
||||
- VOID_USEFUL_LINKS.md
|
||||
- product.json
|
||||
- package.json
|
||||
|
||||
- src/vs/workbench/api/common/{extHost.api.impl.ts | extHostApiCommands.ts}
|
||||
- src/vs/workbench/workbench.common.main.ts
|
||||
- src/vs/workbench/contrib/void
|
||||
- extensions/void
|
||||
- src/vs/workbench/contrib/void/*
|
||||
- extensions/void/*
|
||||
|
||||
- .github/
|
||||
- .vscode/settings
|
||||
- .github/*
|
||||
- .vscode/settings/*
|
||||
- .eslintrc.json
|
||||
- build/hygiene.js
|
||||
- build/lib/i18n.resources.json
|
||||
- build/npm/dirs.js
|
||||
|
||||
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
+1085
-186
File diff suppressed because it is too large
Load Diff
+22
-251
@@ -5,7 +5,7 @@
|
||||
"description": "",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.89.0"
|
||||
"vscode": "^1.92.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
@@ -15,243 +15,7 @@
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "Void",
|
||||
"properties": {
|
||||
"void.whichApi": {
|
||||
"type": "string",
|
||||
"default": "anthropic",
|
||||
"description": "Choose an API provider.",
|
||||
"enum": [
|
||||
"openAI",
|
||||
"openAICompatible",
|
||||
"anthropic",
|
||||
"azure",
|
||||
"ollama",
|
||||
"greptile"
|
||||
]
|
||||
},
|
||||
"void.anthropic.apiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Anthropic API key."
|
||||
},
|
||||
"void.anthropic.model": {
|
||||
"type": "string",
|
||||
"default": "claude-3-5-sonnet-20240620",
|
||||
"description": "Anthropic model to use.",
|
||||
"enum": [
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307"
|
||||
]
|
||||
},
|
||||
"void.anthropic.maxTokens": {
|
||||
"type": "string",
|
||||
"default": "8192",
|
||||
"description": "Anthropic max number of tokens to output.",
|
||||
"enum": [
|
||||
"1024",
|
||||
"2048",
|
||||
"4096",
|
||||
"8192"
|
||||
]
|
||||
},
|
||||
"void.openAI.apiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "OpenAI API key."
|
||||
},
|
||||
"void.openAI.model": {
|
||||
"type": "string",
|
||||
"default": "gpt-4o",
|
||||
"description": "OpenAI model to use.",
|
||||
"enum": [
|
||||
"o1-preview",
|
||||
"o1-mini",
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4o-2024-08-06",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-turbo-2024-04-09",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-0125-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4",
|
||||
"gpt-4-0613",
|
||||
"gpt-3.5-turbo-0125",
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-1106"
|
||||
]
|
||||
},
|
||||
"void.greptile.apiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Greptile API key."
|
||||
},
|
||||
"void.greptile.githubPAT": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Github PAT given to Greptile to access your repository."
|
||||
},
|
||||
"void.greptile.remote": {
|
||||
"type": "string",
|
||||
"description": "remote provider",
|
||||
"enum": [
|
||||
"github",
|
||||
"gitlab"
|
||||
]
|
||||
},
|
||||
"void.greptile.repository": {
|
||||
"type": "string",
|
||||
"description": "Repository identifier in \"owner/repository\" format."
|
||||
},
|
||||
"void.greptile.branch": {
|
||||
"type": "string",
|
||||
"default": "main",
|
||||
"description": "Name of the git branch."
|
||||
},
|
||||
"void.azure.apiKey": {
|
||||
"type": "string",
|
||||
"description": "Azure API key."
|
||||
},
|
||||
"void.azure.deploymentId": {
|
||||
"type": "string",
|
||||
"description": "Azure API deployment ID."
|
||||
},
|
||||
"void.azure.resourceName": {
|
||||
"type": "string",
|
||||
"description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`"
|
||||
},
|
||||
"void.azure.providerSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"baseURL": {
|
||||
"type": "string",
|
||||
"default": "https://${resourceName}.openai.azure.com/openai/deployments",
|
||||
"description": "Azure API base URL."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Custom headers to include in the requests."
|
||||
}
|
||||
}
|
||||
},
|
||||
"void.ollama.endpoint": {
|
||||
"type": "string",
|
||||
"default": "http://127.0.0.1:11434",
|
||||
"description": "The Ollama endpoint. Start Ollama by running `OLLAMA_ORIGINS=\"vscode-webview://*\" ollama serve`"
|
||||
},
|
||||
"void.ollama.model": {
|
||||
"type": "string",
|
||||
"default": "llama3.1",
|
||||
"description": "Ollama model to use.",
|
||||
"enum": [
|
||||
"codegemma",
|
||||
"codegemma:2b",
|
||||
"codegemma:7b",
|
||||
"codellama",
|
||||
"codellama:7b",
|
||||
"codellama:13b",
|
||||
"codellama:34b",
|
||||
"codellama:70b",
|
||||
"codellama:code",
|
||||
"codellama:python",
|
||||
"command-r",
|
||||
"command-r:35b",
|
||||
"command-r-plus",
|
||||
"command-r-plus:104b",
|
||||
"deepseek-coder-v2",
|
||||
"deepseek-coder-v2:16b",
|
||||
"deepseek-coder-v2:236b",
|
||||
"falcon2",
|
||||
"falcon2:11b",
|
||||
"firefunction-v2",
|
||||
"firefunction-v2:70b",
|
||||
"gemma",
|
||||
"gemma:2b",
|
||||
"gemma:7b",
|
||||
"gemma2",
|
||||
"gemma2:2b",
|
||||
"gemma2:9b",
|
||||
"gemma2:27b",
|
||||
"llama2",
|
||||
"llama2:7b",
|
||||
"llama2:13b",
|
||||
"llama2:70b",
|
||||
"llama3",
|
||||
"llama3:8b",
|
||||
"llama3:70b",
|
||||
"llama3-chatqa",
|
||||
"llama3-chatqa:8b",
|
||||
"llama3-chatqa:70b",
|
||||
"llama3-gradient",
|
||||
"llama3-gradient:8b",
|
||||
"llama3-gradient:70b",
|
||||
"llama3.1",
|
||||
"llama3.1:8b",
|
||||
"llama3.1:70b",
|
||||
"llama3.1:405b",
|
||||
"llava",
|
||||
"llava:7b",
|
||||
"llava:13b",
|
||||
"llava:34b",
|
||||
"llava-llama3",
|
||||
"llava-llama3:8b",
|
||||
"llava-phi3",
|
||||
"llava-phi3:3.8b",
|
||||
"mistral",
|
||||
"mistral:7b",
|
||||
"mistral-large",
|
||||
"mistral-large:123b",
|
||||
"mistral-nemo",
|
||||
"mistral-nemo:12b",
|
||||
"mixtral",
|
||||
"mixtral:8x7b",
|
||||
"mixtral:8x22b",
|
||||
"moondream",
|
||||
"moondream:1.8b",
|
||||
"openhermes",
|
||||
"openhermes:v2.5",
|
||||
"phi3",
|
||||
"phi3:3.8b",
|
||||
"phi3:14b",
|
||||
"phi3.5",
|
||||
"phi3.5:3.8b",
|
||||
"qwen",
|
||||
"qwen:7b",
|
||||
"qwen:14b",
|
||||
"qwen:32b",
|
||||
"qwen:72b",
|
||||
"qwen:110b",
|
||||
"qwen2",
|
||||
"qwen2:0.5b",
|
||||
"qwen2:1.5b",
|
||||
"qwen2:7b",
|
||||
"qwen2:72b",
|
||||
"smollm",
|
||||
"smollm:135m",
|
||||
"smollm:360m",
|
||||
"smollm:1.7b"
|
||||
]
|
||||
},
|
||||
"void.openAICompatible.endpoint": {
|
||||
"type": "string",
|
||||
"default": "http://127.0.0.1:11434/v1",
|
||||
"description": "The endpoint."
|
||||
},
|
||||
"void.openAICompatible.model": {
|
||||
"type": "string",
|
||||
"default": "gpt-4o",
|
||||
"description": "The name of the model to use."
|
||||
},
|
||||
"void.openAICompatible.apiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Your API key."
|
||||
}
|
||||
}
|
||||
"properties": {}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
@@ -260,7 +24,7 @@
|
||||
},
|
||||
{
|
||||
"command": "void.ctrl+k",
|
||||
"title": "Show Selection Lens"
|
||||
"title": "Make Inline Edit"
|
||||
},
|
||||
{
|
||||
"command": "void.acceptDiff",
|
||||
@@ -279,8 +43,9 @@
|
||||
"command": "void.toggleThreadSelector",
|
||||
"title": "View past chats",
|
||||
"icon": "$(history)"
|
||||
}, {
|
||||
"command": "void.openSettings",
|
||||
},
|
||||
{
|
||||
"command": "void.toggleSettings",
|
||||
"title": "Void settings",
|
||||
"icon": "$(settings-gear)"
|
||||
}
|
||||
@@ -328,7 +93,7 @@
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "void.openSettings",
|
||||
"command": "void.toggleSettings",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
}
|
||||
@@ -337,13 +102,17 @@
|
||||
},
|
||||
"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",
|
||||
@@ -351,32 +120,34 @@
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/vscode": "1.89.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/vscode": "1.92.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "2.4.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"esbuild": "^0.23.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.35.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"globals": "^15.9.0",
|
||||
"marked": "^14.1.0",
|
||||
"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",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"ollama": "^0.5.9",
|
||||
"openai": "^4.57.0",
|
||||
"diff": "^7.0.0"
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { findDiffs } from './findDiffs';
|
||||
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './shared_types';
|
||||
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './common/shared_types';
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider {
|
||||
// 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)
|
||||
return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
|
||||
}
|
||||
|
||||
// declared by us, registered with vscode.languages.registerCodeLensProvider()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
function getNonce() {
|
||||
function generateNonce() {
|
||||
let text = "";
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
@@ -19,7 +19,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
private readonly _extensionUri: vscode.Uri
|
||||
|
||||
private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration
|
||||
// private _webviewView?: vscode.WebviewView;
|
||||
private _webviewDeps: string[] = [];
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
@@ -28,39 +28,18 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
let temp_res: typeof this._res | undefined = undefined
|
||||
this.webview = new Promise((res, rej) => { temp_res = res })
|
||||
if (!temp_res) throw new Error("sidebar provider: resolver was undefined")
|
||||
if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined")
|
||||
this._res = temp_res
|
||||
|
||||
// if it affects one of the config items webview depends on, update the webview
|
||||
// TODO should be able to move this entirely to React - make updateWebviewHTML mount once, and then send updates via postMessage from then on
|
||||
vscode.workspace.onDidChangeConfiguration(event => {
|
||||
if (this._webviewDeps.map(dep => event.affectsConfiguration(dep)).some(v => !!v)) {
|
||||
if (this._webviewView) {
|
||||
this.updateWebviewHTML(this._webviewView.webview);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// this is updated
|
||||
private updateWebviewHTML(webview: vscode.Webview) {
|
||||
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com'];
|
||||
// called by us
|
||||
updateWebviewHTML(webview: vscode.Webview) {
|
||||
this._webviewDeps = []
|
||||
|
||||
const ollamaEndpoint: string | undefined = vscode.workspace.getConfiguration('void.ollama').get('endpoint');
|
||||
this._webviewDeps.push('void.ollama.endpoint');
|
||||
if (ollamaEndpoint)
|
||||
allowed_urls.push(ollamaEndpoint);
|
||||
|
||||
const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint');
|
||||
this._webviewDeps.push('void.openAICompatible.endpoint');
|
||||
if (openAICompatibleEndpoint)
|
||||
allowed_urls.push(openAICompatibleEndpoint);
|
||||
|
||||
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
|
||||
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
|
||||
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri));
|
||||
const nonce = getNonce();
|
||||
const nonce = generateNonce();
|
||||
|
||||
const webviewHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -68,12 +47,13 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom View</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ${allowed_urls.join(' ')}; img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
|
||||
<meta http-equiv="Content-Security-Policy" content="img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
|
||||
<base href="${rootUri}/">
|
||||
<link href="${stylesUri}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="ctrlkroot"></div>
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
@@ -100,6 +80,6 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
// resolve webview and _webviewView
|
||||
this._res(webview);
|
||||
this._webviewView = webviewView;
|
||||
// this._webviewView = webviewView;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,9 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { Ollama } from 'ollama/browser'
|
||||
import { VoidConfig } from '../sidebar/contextForConfig';
|
||||
|
||||
|
||||
// always compare these against package.json to make sure every setting in this type can actually be provided by the user
|
||||
export type ApiConfig = {
|
||||
anthropic: {
|
||||
apikey: string,
|
||||
model: string,
|
||||
maxTokens: string
|
||||
},
|
||||
openAI: {
|
||||
apikey: string,
|
||||
model: string,
|
||||
},
|
||||
greptile: {
|
||||
apikey: string,
|
||||
githubPAT: string,
|
||||
repoinfo: {
|
||||
remote: string, // e.g. 'github'
|
||||
repository: string, // e.g. 'voideditor/void'
|
||||
branch: string // e.g. 'main'
|
||||
}
|
||||
},
|
||||
ollama: {
|
||||
endpoint: string,
|
||||
model: string
|
||||
},
|
||||
openAICompatible: {
|
||||
endpoint: string,
|
||||
model: string,
|
||||
apikey: string
|
||||
}
|
||||
whichApi: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
type OnText = (newText: string, fullText: string) => void
|
||||
@@ -48,7 +17,8 @@ type SendLLMMessageFnTypeInternal = (params: {
|
||||
messages: LLMMessage[],
|
||||
onText: OnText,
|
||||
onFinalMessage: (input: string) => void,
|
||||
apiConfig: ApiConfig,
|
||||
onError: (error: string) => void,
|
||||
voidConfig: VoidConfig,
|
||||
})
|
||||
=> {
|
||||
abort: () => void
|
||||
@@ -58,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
|
||||
@@ -67,14 +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: apiConfig.anthropic.model,
|
||||
max_tokens: parseInt(apiConfig.anthropic.maxTokens),
|
||||
model: voidConfig.anthropic.model,
|
||||
max_tokens: parseInt(voidConfig.anthropic.maxTokens),
|
||||
messages: messages,
|
||||
});
|
||||
|
||||
@@ -94,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
|
||||
}
|
||||
|
||||
@@ -108,8 +88,8 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||
|
||||
|
||||
|
||||
// OpenAI and OpenAICompatible
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ''
|
||||
@@ -122,17 +102,27 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
if (apiConfig.whichApi === 'openAI') {
|
||||
openai = new OpenAI({ baseURL: apiConfig.openAICompatible.endpoint, apiKey: apiConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
|
||||
options = { model: apiConfig.openAI.model, messages: messages, stream: true, }
|
||||
if (voidConfig.default.whichApi === 'openAI') {
|
||||
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
|
||||
options = { model: voidConfig.openAI.model, messages: messages, stream: true, }
|
||||
}
|
||||
else if (apiConfig.whichApi === 'openAICompatible') {
|
||||
openai = new OpenAI({ apiKey: apiConfig.openAI.apikey, dangerouslyAllowBrowser: true });
|
||||
options = { model: apiConfig.openAICompatible.model, messages: messages, stream: true, }
|
||||
else if (voidConfig.default.whichApi === 'openRouter') {
|
||||
openai = new OpenAI({
|
||||
baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
"X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
});
|
||||
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, }
|
||||
}
|
||||
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: ${apiConfig.whichApi}`)
|
||||
throw new Error(`apiConfig.whichAPI was invalid: ${apiConfig.whichApi}`)
|
||||
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
|
||||
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
@@ -143,27 +133,35 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||
didAbort = true;
|
||||
}
|
||||
// when receive text
|
||||
try {
|
||||
for await (const chunk of response) {
|
||||
if (didAbort) return;
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
for await (const chunk of response) {
|
||||
if (didAbort) return;
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
// when error/fail
|
||||
catch (error) {
|
||||
console.error('Error in OpenAI stream:', error);
|
||||
onFinalMessage(fullText);
|
||||
onFinalMessage(fullText);
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError) {
|
||||
if (error.status === 401) {
|
||||
onError('Invalid API key.');
|
||||
}
|
||||
else {
|
||||
onError(error.message);
|
||||
}
|
||||
}
|
||||
else {
|
||||
onError(error);
|
||||
}
|
||||
})
|
||||
|
||||
return { abort };
|
||||
};
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ""
|
||||
@@ -173,10 +171,10 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
|
||||
didAbort = true;
|
||||
};
|
||||
|
||||
const ollama = new Ollama({ host: apiConfig.ollama.endpoint })
|
||||
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: apiConfig.ollama.model,
|
||||
model: voidConfig.ollama.model,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
})
|
||||
@@ -186,21 +184,20 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
|
||||
didAbort = true
|
||||
}
|
||||
// iterate through the stream
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
if (didAbort) return;
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
}
|
||||
// when error/fail
|
||||
catch (error) {
|
||||
console.error('Error:', error);
|
||||
onFinalMessage(fullText);
|
||||
for await (const chunk of stream) {
|
||||
if (didAbort) return;
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch(error => {
|
||||
onError(error)
|
||||
})
|
||||
|
||||
return { abort };
|
||||
};
|
||||
|
||||
@@ -210,7 +207,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
|
||||
// https://docs.greptile.com/api-reference/query
|
||||
// https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ''
|
||||
@@ -222,14 +219,14 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
|
||||
fetch('https://api.greptile.com/v2/query', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiConfig.greptile.apikey}`,
|
||||
"X-Github-Token": `${apiConfig.greptile.githubPAT}`,
|
||||
"Authorization": `Bearer ${voidConfig.greptile.apikey}`,
|
||||
"X-Github-Token": `${voidConfig.greptile.githubPAT}`,
|
||||
"Content-Type": `application/json`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages,
|
||||
stream: true,
|
||||
repositories: [apiConfig.greptile.repoinfo]
|
||||
repositories: [voidConfig.greptile.repoinfo]
|
||||
}),
|
||||
})
|
||||
// this is {message}\n{message}\n{message}...\n
|
||||
@@ -268,9 +265,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
|
||||
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('Error in Greptile stream:', e);
|
||||
onFinalMessage(fullText);
|
||||
|
||||
onError(e)
|
||||
});
|
||||
|
||||
return { abort }
|
||||
@@ -279,22 +274,22 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
|
||||
|
||||
|
||||
|
||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
if (!apiConfig) return { abort: () => { } }
|
||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
if (!voidConfig) return { abort: () => { } }
|
||||
|
||||
switch (apiConfig.whichApi) {
|
||||
switch (voidConfig.default.whichApi) {
|
||||
case 'anthropic':
|
||||
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig });
|
||||
return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig });
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig });
|
||||
case 'greptile':
|
||||
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig });
|
||||
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig });
|
||||
case 'ollama':
|
||||
return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig });
|
||||
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig });
|
||||
case 'greptile':
|
||||
return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig });
|
||||
default:
|
||||
console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`);
|
||||
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
return { abort: () => { } }
|
||||
//return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ApiConfig } from './common/sendLLMMessage';
|
||||
import { PartialVoidConfig } from '../sidebar/contextForConfig';
|
||||
|
||||
|
||||
|
||||
@@ -38,50 +38,36 @@ type Diff = {
|
||||
lenses: vscode.CodeLens[],
|
||||
} & BaseDiff
|
||||
|
||||
type WebviewMessage = (
|
||||
|
||||
// editor -> sidebar
|
||||
// editor -> sidebar
|
||||
type MessageToSidebar = (
|
||||
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'requestFiles', filepaths: vscode.Uri[] }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'ctrl+k', selection: CodeSelection }
|
||||
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'getApiConfig' }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'apiConfig', apiConfig: ApiConfig }
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'getAllThreads' }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
|
||||
| { type: 'allThreads', threads: ChatThreads }
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'persistThread', thread: ChatThreads[string] }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'startNewThread' }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'toggleThreadSelector' }
|
||||
|
||||
| { type: 'toggleSettings' }
|
||||
| { type: 'deviceId', deviceId: string }
|
||||
)
|
||||
|
||||
// sidebar -> editor
|
||||
type MessageFromSidebar = (
|
||||
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
|
||||
| { type: 'requestFiles', filepaths: vscode.Uri[] }
|
||||
| { type: 'getPartialVoidConfig' }
|
||||
| { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig }
|
||||
| { type: 'getAllThreads' }
|
||||
| { type: 'persistThread', thread: ChatThreads[string] }
|
||||
| { type: 'getDeviceId' }
|
||||
)
|
||||
|
||||
type Command = WebviewMessage['type']
|
||||
|
||||
type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string;
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
}
|
||||
@@ -105,8 +91,8 @@ export {
|
||||
Diff, DiffArea,
|
||||
CodeSelection,
|
||||
File,
|
||||
WebviewMessage,
|
||||
Command,
|
||||
MessageFromSidebar,
|
||||
MessageToSidebar,
|
||||
ChatThreads,
|
||||
ChatMessage,
|
||||
}
|
||||
@@ -1,81 +1,96 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { DisplayChangesProvider } from './DisplayChangesProvider';
|
||||
import { BaseDiffArea, ChatThreads, WebviewMessage } from './shared_types';
|
||||
import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types';
|
||||
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
|
||||
import { ApiConfig } from './common/sendLLMMessage';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
// this comes from vscode.proposed.editorInsets.d.ts
|
||||
declare module 'vscode' {
|
||||
export interface WebviewEditorInset {
|
||||
readonly editor: vscode.TextEditor;
|
||||
readonly line: number;
|
||||
readonly height: number;
|
||||
readonly webview: vscode.Webview;
|
||||
readonly onDidDispose: Event<void>;
|
||||
dispose(): void;
|
||||
}
|
||||
export namespace window {
|
||||
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const readFileContentOfUri = async (uri: vscode.Uri) => {
|
||||
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
|
||||
.replace(/\r\n/g, '\n') // replace windows \r\n with \n
|
||||
}
|
||||
|
||||
|
||||
const getApiConfig = () => {
|
||||
const apiConfig: ApiConfig = {
|
||||
anthropic: {
|
||||
apikey: vscode.workspace.getConfiguration('void.anthropic').get('apiKey') ?? '',
|
||||
model: vscode.workspace.getConfiguration('void.anthropic').get('model') ?? '',
|
||||
maxTokens: vscode.workspace.getConfiguration('void.anthropic').get('maxTokens') ?? '',
|
||||
},
|
||||
openAI: {
|
||||
apikey: vscode.workspace.getConfiguration('void.openAI').get('apiKey') ?? '',
|
||||
model: vscode.workspace.getConfiguration('void.openAI').get('model') ?? '',
|
||||
},
|
||||
greptile: {
|
||||
apikey: vscode.workspace.getConfiguration('void.greptile').get('apiKey') ?? '',
|
||||
githubPAT: vscode.workspace.getConfiguration('void.greptile').get('githubPAT') ?? '',
|
||||
repoinfo: {
|
||||
remote: 'github',
|
||||
repository: 'TODO',
|
||||
branch: 'main'
|
||||
}
|
||||
},
|
||||
ollama: {
|
||||
endpoint: vscode.workspace.getConfiguration('void.ollama').get('endpoint') ?? '',
|
||||
model: vscode.workspace.getConfiguration('void.ollama').get('model') ?? '',
|
||||
},
|
||||
openAICompatible: {
|
||||
endpoint: vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint') ?? '',
|
||||
apikey: vscode.workspace.getConfiguration('void.openAICompatible').get('apiKey') ?? '',
|
||||
model: vscode.workspace.getConfiguration('void.openAICompatible').get('model') ?? '',
|
||||
},
|
||||
whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? ''
|
||||
}
|
||||
return apiConfig
|
||||
const roundRangeToLines = (selection: vscode.Selection) => {
|
||||
return new vscode.Range(selection.start.line, 0, selection.end.line, Number.MAX_SAFE_INTEGER)
|
||||
}
|
||||
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// 1. Mount the chat sidebar
|
||||
const webviewProvider = new SidebarWebviewProvider(context);
|
||||
const sidebarWebviewProvider = new SidebarWebviewProvider(context);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, webviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
|
||||
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
|
||||
);
|
||||
|
||||
// 2. Activate the sidebar on ctrl+l
|
||||
|
||||
|
||||
// 2. ctrl+l
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('void.ctrl+l', () => {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
if (!editor) return
|
||||
|
||||
|
||||
// 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));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -91,33 +106,23 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
displayChangesProvider.rejectDiff(params)
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.openSettings', async () => {
|
||||
vscode.commands.executeCommand('workbench.action.openSettings', '@ext:void.void');
|
||||
}));
|
||||
|
||||
// 5. Receive messages from sidebar
|
||||
webviewProvider.webview.then(
|
||||
sidebarWebviewProvider.webview.then(
|
||||
webview => {
|
||||
|
||||
// top navigation bar commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
|
||||
webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage)
|
||||
webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
|
||||
}))
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
|
||||
webview.postMessage({ type: 'toggleThreadSelector' } satisfies WebviewMessage)
|
||||
webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
|
||||
}))
|
||||
|
||||
// when config changes, send it to the sidebar
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('void')) {
|
||||
const apiConfig = getApiConfig()
|
||||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
||||
}
|
||||
})
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => {
|
||||
webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar)
|
||||
}));
|
||||
|
||||
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
|
||||
webview.onDidReceiveMessage(async (m: WebviewMessage) => {
|
||||
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
|
||||
|
||||
if (m.type === 'requestFiles') {
|
||||
|
||||
@@ -127,9 +132,10 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
)
|
||||
|
||||
// send contents to webview
|
||||
webview.postMessage({ type: 'files', files, } satisfies WebviewMessage)
|
||||
webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
|
||||
|
||||
} else if (m.type === 'applyChanges') {
|
||||
}
|
||||
else if (m.type === 'applyChanges') {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
@@ -137,6 +143,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// create an area to show diffs
|
||||
const diffArea: BaseDiffArea = {
|
||||
startLine: 0, // in ctrl+L the start and end lines are the full document
|
||||
@@ -162,21 +169,33 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
displayChangesProvider.refreshDiffAreas(editor.document.uri)
|
||||
|
||||
}
|
||||
else if (m.type === 'getApiConfig') {
|
||||
const apiConfig = getApiConfig()
|
||||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
||||
else if (m.type === 'getPartialVoidConfig') {
|
||||
const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
|
||||
webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar)
|
||||
}
|
||||
else if (m.type === 'persistPartialVoidConfig') {
|
||||
const partialVoidConfig = m.partialVoidConfig
|
||||
context.globalState.update('partialVoidConfig', partialVoidConfig)
|
||||
}
|
||||
else if (m.type === 'getAllThreads') {
|
||||
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
||||
webview.postMessage({ type: 'allThreads', threads } satisfies WebviewMessage)
|
||||
webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
|
||||
}
|
||||
else if (m.type === 'persistThread') {
|
||||
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
||||
const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
|
||||
context.workspaceState.update('allThreads', updatedThreads)
|
||||
}
|
||||
else if (m.type === 'getDeviceId') {
|
||||
let deviceId = context.globalState.get('void_deviceid')
|
||||
if (!deviceId || typeof deviceId !== 'string') {
|
||||
deviceId = uuidv4()
|
||||
context.globalState.update('void_deviceid', deviceId)
|
||||
}
|
||||
webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar)
|
||||
}
|
||||
else {
|
||||
console.error('unrecognized command', m.type, m)
|
||||
console.error('unrecognized command', m)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
// import { diffLines, Change } from 'diff';
|
||||
import { BaseDiff } from './shared_types';
|
||||
import { BaseDiff } from './common/shared_types';
|
||||
|
||||
import { diff_match_patch } from 'diff-match-patch';
|
||||
|
||||
|
||||
@@ -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,317 +1,64 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
|
||||
import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"
|
||||
import { File, CodeSelection, WebviewMessage, ChatMessage } from "../shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
|
||||
|
||||
import { marked } from 'marked';
|
||||
import MarkdownRender from "./markdown/MarkdownRender";
|
||||
import BlockCode from "./markdown/BlockCode";
|
||||
|
||||
import * as vscode from 'vscode'
|
||||
import { SelectedFiles } from "./components/SelectedFiles";
|
||||
import { useThreads } from "./threadsContext";
|
||||
|
||||
|
||||
const filesStr = (fullFiles: File[]) => {
|
||||
return fullFiles.map(({ filepath, content }) =>
|
||||
`
|
||||
${filepath.fsPath}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\``).join('\n')
|
||||
}
|
||||
|
||||
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
|
||||
return `
|
||||
${filesStr(files)}
|
||||
|
||||
${!selection ? '' : `
|
||||
I am currently selecting this code:
|
||||
\`\`\`${selection.selectionStr}\`\`\`
|
||||
`}
|
||||
|
||||
Please edit the code following these instructions (or, if appropriate, answer my question instead):
|
||||
${instructions}
|
||||
|
||||
If you make a change, rewrite the entire file.
|
||||
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
const children = chatMessage.displayContent
|
||||
|
||||
if (!children)
|
||||
return null
|
||||
|
||||
let chatbubbleContents: React.ReactNode
|
||||
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles files={chatMessage.files} setFiles={null} />
|
||||
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
|
||||
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
|
||||
}
|
||||
|
||||
|
||||
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
||||
{chatbubbleContents}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const ThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
const { allThreads, currentThread, switchToThread } = useThreads()
|
||||
return (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-right">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/* iterate through all past threads */}
|
||||
{Object.keys(allThreads ?? {}).map((threadId) => {
|
||||
const pastThread = (allThreads ?? {})[threadId];
|
||||
return (
|
||||
<button
|
||||
key={pastThread.id}
|
||||
className={`btn btn-sm btn-secondary ${pastThread.id === currentThread?.id ? "btn-primary" : ""}`}
|
||||
onClick={() => switchToThread(pastThread.id)}
|
||||
>
|
||||
{new Date(pastThread.createdAt).toLocaleString()}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { CodeSelection, ChatMessage, MessageToSidebar } from "../common/shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi"
|
||||
|
||||
import { SidebarThreadSelector } from "./SidebarThreadSelector";
|
||||
import { SidebarChat } from "./SidebarChat";
|
||||
import { SidebarSettings } from './SidebarSettings';
|
||||
import { identifyUser } from "./metrics/posthog";
|
||||
|
||||
|
||||
const Sidebar = () => {
|
||||
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
|
||||
|
||||
// state of current message
|
||||
const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
|
||||
const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
|
||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// state of chat
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
|
||||
const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat')
|
||||
|
||||
const abortFnRef = useRef<(() => void) | null>(null)
|
||||
// if they pressed the + to add a new chat
|
||||
useOnVSCodeMessage('startNewThread', (m) => {
|
||||
setTab('chat');
|
||||
chatInputRef.current?.focus();
|
||||
})
|
||||
|
||||
const [apiConfig, setApiConfig] = useState<ApiConfig | null>(null)
|
||||
// ctrl+l should switch back to chat
|
||||
useOnVSCodeMessage('ctrl+l', (m) => {
|
||||
setTab('chat');
|
||||
chatInputRef.current?.focus();
|
||||
})
|
||||
|
||||
// get Api Config on mount
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: 'getApiConfig' })
|
||||
}, [])
|
||||
// if they toggled thread selector
|
||||
useOnVSCodeMessage('toggleThreadSelector', (m) => {
|
||||
if (tab === 'threadSelector') {
|
||||
setTab('chat')
|
||||
chatInputRef.current?.blur();
|
||||
} else
|
||||
setTab('threadSelector')
|
||||
})
|
||||
|
||||
// Receive messages from the extension
|
||||
useEffect(() => {
|
||||
const listener = (event: MessageEvent) => {
|
||||
|
||||
const m = event.data as WebviewMessage;
|
||||
// resolve any awaiting promises
|
||||
// eg. it will resolve the promise below for `await VSCodeResponse('files')`
|
||||
resolveAwaitingVSCodeResponse(m)
|
||||
|
||||
// if user pressed ctrl+l, add their selection to the sidebar
|
||||
if (m.type === 'ctrl+l') {
|
||||
setSelection(m.selection)
|
||||
const filepath = m.selection.filePath
|
||||
|
||||
// add current file to the context if it's not already in the files array
|
||||
if (!files.find(f => f.fsPath === filepath.fsPath))
|
||||
setFiles(files => [...files, filepath])
|
||||
|
||||
}
|
||||
// when get apiConfig, set
|
||||
else if (m.type === 'apiConfig') {
|
||||
setApiConfig(m.apiConfig)
|
||||
}
|
||||
|
||||
// if they pressed the + to add a new chat
|
||||
else if (m.type === 'startNewThread') {
|
||||
setIsThreadSelectorOpen(false)
|
||||
if (currentThread?.messages.length !== 0)
|
||||
startNewThread()
|
||||
}
|
||||
|
||||
// if they opened thread selector
|
||||
else if (m.type === 'toggleThreadSelector') {
|
||||
setIsThreadSelectorOpen(v => !v)
|
||||
}
|
||||
|
||||
}
|
||||
window.addEventListener('message', listener);
|
||||
return () => { window.removeEventListener('message', listener) }
|
||||
}, [files, selection, startNewThread, currentThread])
|
||||
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
if (isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
setInstructions('');
|
||||
formRef.current?.reset(); // reset the form's text
|
||||
setSelection(null)
|
||||
setFiles([])
|
||||
|
||||
// request file content from vscode and await response
|
||||
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
|
||||
const relevantFiles = await awaitVSCodeResponse('files')
|
||||
|
||||
// add message to chat history
|
||||
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
|
||||
// console.log('prompt:\n', content)
|
||||
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
|
||||
// send message to LLM
|
||||
let { abort } = sendLLMMessage({
|
||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
|
||||
onText: (newText, fullText) => setMessageStream(fullText),
|
||||
onFinalMessage: (content) => {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
|
||||
// clear selection
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
},
|
||||
apiConfig: apiConfig
|
||||
})
|
||||
abortFnRef.current = abort
|
||||
|
||||
}
|
||||
|
||||
const onStop = useCallback(() => {
|
||||
// abort claude
|
||||
abortFnRef.current?.()
|
||||
|
||||
// if messageStream was not empty, add it to the history
|
||||
const llmContent = messageStream || '(canceled)'
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
}, [addMessageToHistory, messageStream])
|
||||
|
||||
//Clear code selection
|
||||
const clearSelection = () => {
|
||||
setSelection(null);
|
||||
};
|
||||
// if they toggled settings
|
||||
useOnVSCodeMessage('toggleSettings', (m) => {
|
||||
if (tab === 'settings') {
|
||||
setTab('chat')
|
||||
chatInputRef.current?.blur();
|
||||
} else
|
||||
setTab('settings')
|
||||
})
|
||||
|
||||
return <>
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
{isThreadSelectorOpen && (
|
||||
<div className="mb-2 max-h-[30vh] overflow-y-auto">
|
||||
<ThreadSelector onClose={() => setIsThreadSelectorOpen(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-y-auto overflow-x-hidden space-y-4">
|
||||
{/* previous messages */}
|
||||
{currentThread !== null && currentThread.messages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
|
||||
<div className={`flex flex-col h-screen w-full`}>
|
||||
|
||||
<div className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
|
||||
<SidebarThreadSelector onClose={() => setTab('chat')} />
|
||||
</div>
|
||||
{/* chatbar */}
|
||||
<div className="shrink-0 py-4">
|
||||
{/* selection */}
|
||||
<div className="text-left">
|
||||
|
||||
<div className="relative">
|
||||
<div className="input">
|
||||
{/* selection */}
|
||||
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
|
||||
{/* selected files */}
|
||||
<SelectedFiles files={files} setFiles={setFiles} />
|
||||
{/* selected code */}
|
||||
{!!selection?.selectionStr && (
|
||||
<BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)} />
|
||||
)}
|
||||
</div>}
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-row items-center rounded-md p-2"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||
|
||||
onSubmit={(e) => {
|
||||
console.log('submit!')
|
||||
e.preventDefault();
|
||||
onSubmit(e)
|
||||
}}>
|
||||
{/* input */}
|
||||
|
||||
<textarea
|
||||
onChange={(e) => { setInstructions(e.target.value) }}
|
||||
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
||||
placeholder="Ctrl+L to select"
|
||||
rows={1}
|
||||
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
|
||||
/>
|
||||
{/* submit button */}
|
||||
{isLoading ?
|
||||
<button
|
||||
onClick={onStop}
|
||||
className="btn btn-primary rounded-r-lg max-h-10 p-2"
|
||||
type='button'
|
||||
>Stop</button>
|
||||
: <button
|
||||
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||
disabled={!instructions}
|
||||
type='submit'
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
<polyline points="5 12 12 5 19 12"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${tab !== 'chat' && tab !== 'threadSelector' ? 'hidden' : ''}`}>
|
||||
<SidebarChat chatInputRef={chatInputRef} />
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
|
||||
<SidebarSettings />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from "react"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
const getBasename = (pathStr: string) => {
|
||||
// "unixify" path
|
||||
pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
|
||||
const parts = pathStr.split("/") // split on /
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
|
||||
return (
|
||||
files.length !== 0 && (
|
||||
<div className="flex flex-wrap -mx-1 -mb-1">
|
||||
{files.map((filename, i) => (
|
||||
<button
|
||||
key={filename.path}
|
||||
disabled={!setFiles}
|
||||
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
|
||||
type="button"
|
||||
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
|
||||
>
|
||||
<span>{getBasename(filename.fsPath)}</span>
|
||||
|
||||
{/* X button */}
|
||||
{!!setFiles && <span className="">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
|
||||
+18
-12
@@ -1,9 +1,10 @@
|
||||
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
|
||||
import { ChatMessage, ChatThreads } from "../shared_types"
|
||||
import { ChatMessage, ChatThreads } from "../common/shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
|
||||
|
||||
|
||||
type ThreadsContextValue = {
|
||||
// a "thread" means a chat message history
|
||||
type ConfigForThreadsValueType = {
|
||||
readonly allThreads: ChatThreads | null,
|
||||
readonly currentThread: ChatThreads[string] | null;
|
||||
addMessageToHistory: (message: ChatMessage) => void;
|
||||
@@ -11,13 +12,17 @@ type ThreadsContextValue = {
|
||||
startNewThread: () => void;
|
||||
}
|
||||
|
||||
const ThreadsContext = createContext<ThreadsContextValue>(undefined as unknown as ThreadsContextValue)
|
||||
const ThreadsContext = createContext<ConfigForThreadsValueType>(undefined as unknown as ConfigForThreadsValueType)
|
||||
|
||||
const createNewThread = () => ({
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
messages: [],
|
||||
})
|
||||
const createNewThread = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// const [stateRef, setState] = useInstantState(initVal)
|
||||
@@ -39,7 +44,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// this loads allThreads in on mount
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: "getAllThreads" })
|
||||
getVSCodeAPI().postMessage({ type: 'getAllThreads' })
|
||||
awaitVSCodeResponse('allThreads')
|
||||
.then(response => {
|
||||
setAllThreads(response.threads)
|
||||
@@ -66,6 +71,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
|
||||
...allThreads.current,
|
||||
[currentThread.id]: {
|
||||
...currentThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...currentThread.messages, message],
|
||||
}
|
||||
})
|
||||
@@ -90,10 +96,10 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function useThreads(): ThreadsContextValue {
|
||||
const context = useContext<ThreadsContextValue>(ThreadsContext)
|
||||
export function useThreads(): ConfigForThreadsValueType {
|
||||
const context = useContext<ConfigForThreadsValueType>(ThreadsContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useThreads must be used within a ThreadsProvider")
|
||||
throw new Error("useThreads missing Provider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -1,48 +1,83 @@
|
||||
import { Command, WebviewMessage } from "../shared_types";
|
||||
import { useEffect } from "react";
|
||||
import { MessageFromSidebar, MessageToSidebar, } from "../common/shared_types";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
type Command = MessageToSidebar['type']
|
||||
|
||||
// message -> res[]
|
||||
const awaiting: { [c in Command]: ((res: any) => void)[] } = {
|
||||
// messageType -> res[]
|
||||
const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = {
|
||||
"ctrl+l": [],
|
||||
"applyChanges": [],
|
||||
"requestFiles": [],
|
||||
"ctrl+k": [],
|
||||
"files": [],
|
||||
"apiConfig": [],
|
||||
"getApiConfig": [],
|
||||
"partialVoidConfig": [],
|
||||
"startNewThread": [],
|
||||
"getAllThreads": [],
|
||||
"allThreads": [],
|
||||
"persistThread": [],
|
||||
"toggleThreadSelector": []
|
||||
"toggleThreadSelector": [],
|
||||
"toggleSettings": [],
|
||||
"deviceId": [],
|
||||
}
|
||||
|
||||
// messageType -> id -> res
|
||||
const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = {
|
||||
"ctrl+l": {},
|
||||
"ctrl+k": {},
|
||||
"files": {},
|
||||
"partialVoidConfig": {},
|
||||
"startNewThread": {},
|
||||
"allThreads": {},
|
||||
"toggleThreadSelector": {},
|
||||
"toggleSettings": {},
|
||||
"deviceId": {}
|
||||
}
|
||||
|
||||
|
||||
// use this function to await responses
|
||||
export const awaitVSCodeResponse = <C extends Command>(c: C) => {
|
||||
let result: Promise<WebviewMessage & { type: C }> = new Promise((res, rej) => {
|
||||
awaiting[c].push(res)
|
||||
let result: Promise<MessageToSidebar & { type: C }> = new Promise((res, rej) => {
|
||||
onetimeCallbacks[c].push(res)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export const resolveAwaitingVSCodeResponse = (m: WebviewMessage) => {
|
||||
// resolve all promises for this message
|
||||
for (let res of awaiting[m.type]) {
|
||||
|
||||
// use this function to add a listener to a certain type of message
|
||||
export const useOnVSCodeMessage = <C extends Command>(messageType: C, fn: (e: MessageToSidebar & { type: C }) => void) => {
|
||||
useEffect(() => {
|
||||
const mType = messageType
|
||||
const callbackId: string = uuidv4();
|
||||
// @ts-ignore
|
||||
callbacks[mType][callbackId] = fn;
|
||||
return () => { delete callbacks[mType][callbackId] }
|
||||
}, [messageType, fn])
|
||||
}
|
||||
|
||||
|
||||
|
||||
// this function gets called whenever sidebar receives a message - it should only mount once
|
||||
export const onMessageFromVSCode = (m: MessageToSidebar) => {
|
||||
// resolve all promises for this message type
|
||||
for (let res of onetimeCallbacks[m.type]) {
|
||||
res(m)
|
||||
onetimeCallbacks[m.type].splice(0) // clear the array
|
||||
}
|
||||
// call the listener for this message type
|
||||
for (let res of Object.values(callbacks[m.type])) {
|
||||
res(m)
|
||||
awaiting[m.type].splice(0) // clear the array
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// VS Code exposes the function acquireVsCodeApi() to us, it should only get called once
|
||||
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
|
||||
|
||||
type AcquireVsCodeApiType = () => {
|
||||
postMessage(message: WebviewMessage): void;
|
||||
postMessage(message: MessageFromSidebar): void;
|
||||
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
|
||||
// getState(): any;
|
||||
};
|
||||
|
||||
// VS Code exposes the function acquireVsCodeApi() to us, this variable makes sure it only gets called once
|
||||
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
|
||||
|
||||
export function getVSCodeAPI(): ReturnType<AcquireVsCodeApiType> {
|
||||
if (vsCodeApi)
|
||||
return vsCodeApi;
|
||||
|
||||
@@ -1,20 +1,66 @@
|
||||
import * as React from "react"
|
||||
import { useEffect } from "react"
|
||||
import * as ReactDOM from "react-dom/client"
|
||||
import Sidebar from "./Sidebar"
|
||||
import { ThreadsProvider } from "./threadsContext"
|
||||
import { CtrlK } from "./CtrlK"
|
||||
import { ThreadsProvider } from "./contextForThreads"
|
||||
import { ConfigProvider } from "./contextForConfig"
|
||||
import { MessageToSidebar } from "../common/shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi"
|
||||
import { identifyUser, initPosthog } from "./metrics/posthog"
|
||||
|
||||
// mount the sidebar on the id="root" element
|
||||
if (typeof document === "undefined") {
|
||||
console.log("index.tsx error: document was undefined")
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root")!
|
||||
console.log("Void root Element:", rootElement)
|
||||
|
||||
const extension = (
|
||||
<ThreadsProvider>
|
||||
<Sidebar />
|
||||
</ThreadsProvider>
|
||||
)
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(extension)
|
||||
const CommonEffects = () => {
|
||||
// initialize posthog
|
||||
useEffect(() => {
|
||||
initPosthog()
|
||||
}, [])
|
||||
|
||||
// when we get the deviceid, identify the user
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: 'getDeviceId' });
|
||||
awaitVSCodeResponse('deviceId').then((m => {
|
||||
identifyUser(m.deviceId)
|
||||
}))
|
||||
}, [])
|
||||
|
||||
// Receive messages from the VSCode extension
|
||||
useEffect(() => {
|
||||
const listener = (event: MessageEvent) => {
|
||||
const m = event.data as MessageToSidebar;
|
||||
onMessageFromVSCode(m)
|
||||
}
|
||||
window.addEventListener('message', listener);
|
||||
return () => window.removeEventListener('message', listener)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
(() => {
|
||||
// mount the sidebar on the id="root" element
|
||||
const rootElement = document.getElementById("root")!
|
||||
console.log("Void root Element:", rootElement)
|
||||
|
||||
const sidebar = (<>
|
||||
<CommonEffects />
|
||||
|
||||
<ThreadsProvider>
|
||||
<ConfigProvider>
|
||||
<Sidebar />
|
||||
</ConfigProvider>
|
||||
</ThreadsProvider>
|
||||
|
||||
<ConfigProvider>
|
||||
<CtrlK />
|
||||
</ConfigProvider>
|
||||
|
||||
</>)
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(sidebar)
|
||||
})();
|
||||
|
||||
|
||||
@@ -1,79 +1,41 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||
import { getVSCodeAPI } from "../getVscodeApi"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
|
||||
enum CopyButtonState {
|
||||
Copy = "Copy",
|
||||
Copied = "Copied!",
|
||||
Error = "Could not copy",
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000
|
||||
const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
|
||||
// code block with toolbar (Apply, Copy, etc) at top
|
||||
const BlockCode = ({
|
||||
text,
|
||||
toolbar,
|
||||
hideToolbar = false,
|
||||
className,
|
||||
}: {
|
||||
text: string
|
||||
toolbar?: ReactNode
|
||||
hideToolbar?: boolean
|
||||
className?: string
|
||||
}) => {
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const customStyle = {
|
||||
...atomOneDarkReasonable,
|
||||
'code[class*="language-"]': {
|
||||
...atomOneDarkReasonable['code[class*="language-"]'],
|
||||
background: "none",
|
||||
},
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
setCopyButtonState(CopyButtonState.Copy)
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}
|
||||
}, [copyButtonState])
|
||||
return (<>
|
||||
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Copied)
|
||||
},
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Error)
|
||||
}
|
||||
)
|
||||
}, [text])
|
||||
|
||||
const defaultToolbar = (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonState}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={async () => {
|
||||
getVSCodeAPI().postMessage({ type: "applyChanges", code: text })
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
{!hideToolbar && (
|
||||
<div className="absolute top-0 right-0 invisible group-hover:visible">
|
||||
<div className="flex space-x-2 p-2">{toolbar || defaultToolbar}</div>
|
||||
{!toolbar ? null : (
|
||||
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className="flex space-x-2 p-2">{buttonsOnHover === null ? null : buttonsOnHover}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
|
||||
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg`}
|
||||
>
|
||||
<pre className="p-2">{text}</pre>
|
||||
<SyntaxHighlighter
|
||||
language={language ?? 'plaintext'} // TODO must auto detect language
|
||||
style={customStyle}
|
||||
className={"rounded-sm"}
|
||||
>
|
||||
{text}
|
||||
</SyntaxHighlighter>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
import React, { JSX } from "react"
|
||||
import React, { JSX, useCallback, useEffect, useState } from "react"
|
||||
import { marked, MarkedToken, Token, TokensList } from "marked"
|
||||
import BlockCode from "./BlockCode"
|
||||
import { getVSCodeAPI } from "../getVscodeApi"
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
Copy = "Copy",
|
||||
Copied = "Copied!",
|
||||
Error = "Could not copy",
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
|
||||
useEffect(() => {
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
setCopyButtonState(CopyButtonState.Copy)
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}
|
||||
}, [copyButtonState])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Copied)
|
||||
},
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Error)
|
||||
}
|
||||
)
|
||||
}, [text])
|
||||
|
||||
return <>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonState}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={async () => {
|
||||
getVSCodeAPI().postMessage({ type: "applyChanges", code: text })
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
|
||||
|
||||
@@ -12,7 +63,11 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
return <BlockCode text={t.text} />
|
||||
return <BlockCode
|
||||
text={t.text}
|
||||
language={t.lang}
|
||||
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
@@ -165,4 +220,4 @@ const MarkdownRender = ({ string, nested = false }: { string: string, nested?: b
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownRender
|
||||
export default MarkdownRender
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
|
||||
export const identifyUser = (id: string) => {
|
||||
posthog.identify(id)
|
||||
}
|
||||
|
||||
export const captureEvent = (eventId: string, properties: object) => {
|
||||
posthog.capture(eventId, properties)
|
||||
}
|
||||
|
||||
export const initPosthog = () => {
|
||||
// We send absolutely no code to the server. We only track usage metrics like button clicks, etc. This might change and we might eventually add an opt-in or opt-out.
|
||||
posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2',
|
||||
{
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -29,11 +29,16 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
/* add transparency when disabled */
|
||||
&:disabled {
|
||||
@apply opacity-75 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
|
||||
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border focus:outline-vscode-focus-border;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@apply bg-vscode-dropdown-bg text-vscode-dropdown-foreground border-vscode-dropdown-border focus:outline-vscode-focus-border;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,11 @@ module.exports = {
|
||||
"button-hoverBg": "var(--vscode-button-hoverBackground)",
|
||||
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
|
||||
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
|
||||
"button-secondary-hoverBg":
|
||||
"var(--vscode-button-secondaryHoverBackground)",
|
||||
"button-secondary-hoverBg": "var(--vscode-button-secondaryHoverBackground)",
|
||||
"dropdown-bg": "var(--vscode-settings-dropdownBackground)",
|
||||
"dropdown-foreground": "var(--vscode-settings-dropdownForeground)",
|
||||
"dropdown-border": "var(--vscode-settings-dropdownBorder)",
|
||||
"focus-border": "var(--vscode-focusBorder)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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