Compare commits
217 Commits
fix-cors
...
new-services
| Author | SHA1 | Date | |
|---|---|---|---|
| 64760aae13 | |||
| d0d1124543 | |||
| 90a3fd0a21 | |||
| 11ed2545c7 | |||
| 3d1df92f6a | |||
| 4ca16caa4e | |||
| 06b5cca59a | |||
| e3c292eee7 | |||
| 99c3786d06 | |||
| 6e40a92315 | |||
| a9b2ff631d | |||
| ef8bba00b9 | |||
| ff7830e704 | |||
| 01a345a391 | |||
| 33909b3143 | |||
| 9a9d0da91e | |||
| 1df044fb02 | |||
| dfbd729fa4 | |||
| ef9d0daef4 | |||
| ffe5a41b01 | |||
| 372beb7a2d | |||
| 406f29bb5a | |||
| ee8b98ff74 | |||
| ffb0549c7c | |||
| 8f9c2f0630 | |||
| 84bfc26dce | |||
| 80a87ff3c6 | |||
| 806ec939e5 | |||
| 98a22d2eb6 | |||
| ffa31bc289 | |||
| a1ab8a5796 | |||
| 70c7d0261c | |||
| e7af8d9448 | |||
| e1feaba74a | |||
| 8456f979ba | |||
| 6630d596f6 | |||
| a435a59a2e | |||
| 9cf4980004 | |||
| 6298be394d | |||
| e647ce0054 | |||
| 5fb30fed87 | |||
| 7cc57bf086 | |||
| 3fb6a4890d | |||
| 97fd332c4e | |||
| 81282e16b3 | |||
| 382275fa9f | |||
| 91f8321278 | |||
| 9b1f21d7c1 | |||
| 739ac2562b | |||
| 298423928e | |||
| 702d11ed3e | |||
| 4513b732d6 | |||
| 78e72f0c80 | |||
| f457f006fb | |||
| d6074e7ab5 | |||
| 3669401819 | |||
| ee7d0ed836 | |||
| ba87b4f0de | |||
| e4bc21ffb1 | |||
| 81b78b7765 | |||
| c3c53b5e81 | |||
| f49a6f8e41 | |||
| 5a26aa8746 | |||
| 8661fe1417 | |||
| 2a519157da | |||
| ac8365def4 | |||
| 610b52587b | |||
| 01efab6a1d | |||
| 7f8e1ff00d | |||
| 5b55eed08a | |||
| d618223289 | |||
| 7752278720 | |||
| 597c04f63b | |||
| a7c4a1b179 | |||
| a7aab9cf86 | |||
| c44497510b | |||
| c485d2c478 | |||
| dfdf29fb36 | |||
| 420fb1a186 | |||
| 78a48bae13 | |||
| fc187e0028 | |||
| f973f8d4e7 | |||
| 8403eed8ca | |||
| 718b8cc61b | |||
| f6924df09b | |||
| 8b25f80e9b | |||
| a42ff2d59a | |||
| 0589a2a5ec | |||
| 7dda4bdc08 | |||
| f21ef61677 | |||
| 56b6d406d6 | |||
| 2ae29a50e5 | |||
| 9673e974ea | |||
| 842d388669 | |||
| f146385e1d | |||
| bb87726b34 | |||
| deb30b49bc | |||
| 56923eec52 | |||
| d2692d2594 | |||
| 5d44fdc17d | |||
| aada60c5be | |||
| b53e5d4886 | |||
| f299998994 | |||
| 967292e615 | |||
| 19e3ebefda | |||
| 7cf6c9d798 | |||
| 14e314e5b8 | |||
| 4e6e0b3bde | |||
| 63b086b786 | |||
| bda6ad06c9 | |||
| 829d82850d | |||
| 3991372f5b | |||
| 9b427c78bf | |||
| 5dad4c8f06 | |||
| e2d39fe4b1 | |||
| 1365400f21 | |||
| 94f4d2caaf | |||
| 696abc39fd | |||
| abc7151cd6 | |||
| c86f47743e | |||
| 9d6eb62c3e | |||
| 2f26b34a96 | |||
| 5807c94a61 | |||
| df23314bd2 | |||
| 6703aa1545 | |||
| aaab289ece | |||
| d809839530 | |||
| 9e583960ae | |||
| 19737d4cdc | |||
| 901579a445 | |||
| 051f8f38e7 | |||
| 07fcb3a72c | |||
| 62cdaba44b | |||
| 3147a92463 | |||
| d175ac20ef | |||
| 42a2dec781 | |||
| dbef72c01c | |||
| 6ddf977c5d | |||
| 522f136872 | |||
| a28194c601 | |||
| cb2de5505b | |||
| f84c964d10 | |||
| bb4f99f687 | |||
| 49b43820b2 | |||
| b22bbf9639 | |||
| a1ff458007 | |||
| fb263f9d81 | |||
| 72deba43dd | |||
| 24fdb1ca0d | |||
| f20f41c804 | |||
| 66037e2049 | |||
| adb504f2e0 | |||
| 8b90d7827d | |||
| 62e1657274 | |||
| 7f776eebdb | |||
| 0db0513752 | |||
| d8b6b3c306 | |||
| f7c9d732f5 | |||
| 8a95b13711 | |||
| 55884ed1c9 | |||
| 582b25dc44 | |||
| 252af4c863 | |||
| e5ac36facd | |||
| 77fc2d2d4a | |||
| 298adac06e | |||
| 0f920fb867 | |||
| 02112fbc4f | |||
| 05241649ff | |||
| 4ba00f6216 | |||
| 296cd1b477 | |||
| 2148fe77ec | |||
| 1f692ff01a | |||
| 8e4f943e30 | |||
| a52eba91c9 | |||
| 695d6a1e30 | |||
| a647530d77 | |||
| 18e3f19512 | |||
| 86294c676a | |||
| 7cfd9e5580 | |||
| d29eca457a | |||
| 0855376e02 | |||
| e1692758c6 | |||
| 411167ee44 | |||
| 389efb4701 | |||
| 48f9ac2ba1 | |||
| f9198d4d97 | |||
| eafc3dc0c9 | |||
| f2b176dfda | |||
| 02781927e1 | |||
| 4b3f881e1f | |||
| f3653c1c48 | |||
| 3a1955901e | |||
| e3ce68d2d6 | |||
| ab3d44387e | |||
| 96e24a89e6 | |||
| 25cc6aeb3b | |||
| 4cac92689a | |||
| 7444488278 | |||
| a6cf723dbe | |||
| 7e22b80986 | |||
| 9ec4f3da01 | |||
| ef9ac7a9e3 | |||
| 48426403f3 | |||
| 86c9a17f8d | |||
| d21f2c4d62 | |||
| 733256feab | |||
| f9ce9de316 | |||
| 4ea1bfa9c3 | |||
| a3e337fe2d | |||
| d12a5f3b38 | |||
| 68b5187f7c | |||
| 5140c79dcf | |||
| 91c65b6060 | |||
| aa6b5c6b6c | |||
| cc3a08e1ab | |||
| 5b3a6d240e | |||
| 8897feb290 |
@@ -10,6 +10,7 @@
|
||||
"jsdoc",
|
||||
"header",
|
||||
"local"
|
||||
// "react" // Void
|
||||
],
|
||||
"rules": {
|
||||
"constructor-super": "warn",
|
||||
|
||||
@@ -21,3 +21,4 @@ vscode.db
|
||||
product.overrides.json
|
||||
*.snap.actual
|
||||
.vscode-test
|
||||
.tmp/
|
||||
|
||||
+50
-61
@@ -4,19 +4,19 @@ Welcome! 👋 This is the official guide on how to contribute to Void. We want t
|
||||
|
||||
There are a few ways to contribute:
|
||||
|
||||
- Suggest New Features ([Discord](https://discord.gg/RSNjgaugJs))
|
||||
- Build New Features ([Project](https://github.com/orgs/voideditor/projects/2/views/3))
|
||||
- Submit Issues/Docs/Bugs ([Issues](https://github.com/voideditor/void/issues))
|
||||
- 👨💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
|
||||
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension.
|
||||
|
||||
Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
|
||||
|
||||
|
||||
|
||||
## Building the full IDE
|
||||
|
||||
Please follow the steps below to build the IDE. If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new) with any build errors, or refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
|
||||
### a. Build Prerequisites - Mac
|
||||
|
||||
If you're using a Mac, make sure you have Python and XCode installed (you probably do by default).
|
||||
If you're using a Mac, you need Python and XCode. You probably have these by default.
|
||||
|
||||
### b. Build Prerequisites - Windows
|
||||
|
||||
@@ -27,38 +27,39 @@ Go to the "Workloads" tab and select:
|
||||
- `Node.js build tools`
|
||||
|
||||
Go to the "Individual Components" tab and select:
|
||||
- `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`,
|
||||
- `C++ ATL for latest build tools with Spectre Mitigations`,
|
||||
- `C++ MFC for latest build tools with Spectre Mitigations`.
|
||||
- `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`
|
||||
- `C++ ATL for latest build tools with Spectre Mitigations`
|
||||
- `C++ MFC for latest build tools with Spectre Mitigations`
|
||||
|
||||
Finally, click Install.
|
||||
|
||||
### c. Build Prerequisites - Linux
|
||||
|
||||
First, make sure you've installed NodeJS and run `npm install -g node-gyp`. Then:
|
||||
- Debian (Ubuntu, etc) - `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
|
||||
- Red Hat (Fedora, etc) - `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- Others - see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
First, run `npm install -g node-gyp`. Then:
|
||||
|
||||
### Build instructions
|
||||
- Debian (Ubuntu, etc): `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
|
||||
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
|
||||
To build Void, first follow the prerequisite steps above for your operating system and open `void/` inside VSCode. Then:
|
||||
### Building Void
|
||||
|
||||
1. Install all dependencies.
|
||||
To build Void, open `void/` inside VSCode. Then:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
1. `npm install` to install all dependencies.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React.
|
||||
3. Build.
|
||||
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
|
||||
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
|
||||
- This step can take ~5 min. The build is done when you see two check marks.
|
||||
4. Run.
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.bat` (Windows).
|
||||
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
|
||||
|
||||
2. Run `cd ./src/vs/workbench/contrib/void/browser/react/` and then `node ./build.js` to build Void's external dependencies (our React components, etc).
|
||||
|
||||
3. Press <kbd>Ctrl+Shift+B</kbd>, or if you prefer using the terminal run `npm run watch`.
|
||||
#### Building Void from Terminal
|
||||
|
||||
This can take ~5 min.
|
||||
|
||||
If you ran <kbd>Ctrl+Shift+B</kbd>, the build is done when you see two check marks.
|
||||
|
||||
If you ran `npm run watch`, the build is done when you see something like this:
|
||||
Alternatively, if you want to build Void from the terminal, instead of pressing <kbd>Cmd+Shift+B</kbd> you can run `npm run watch`. The build is done when you see something like this:
|
||||
|
||||
```
|
||||
[watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms
|
||||
@@ -67,61 +68,49 @@ If you ran `npm run watch`, the build is done when you see something like this:
|
||||
[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. -->
|
||||
|
||||
4. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This should open up the built IDE!
|
||||
You can always press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building.
|
||||
|
||||
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 follow the prerequisite steps.
|
||||
- Make sure you have the same NodeJS version as `.nvmrc`.
|
||||
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build.
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`.
|
||||
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
- If you see `[ERROR] Cannot start service: Host version "0.23.1" does not match binary version "0.23.0"`, run `npm i -D esbuild@0.23.0` or do a clean install of your npm dependencies.
|
||||
|
||||
|
||||
## Bundling
|
||||
|
||||
To bundle the IDE into an executable, run `npm run gulp vscode-darwin-arm64`.
|
||||
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` (or similar) in the repo's parent's directory. Be patient - compiling can take ~25 minutes.
|
||||
|
||||
Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin-arm64 | linux-ia32 | linux-x64 | linux-arm}(-min)`
|
||||
### Mac
|
||||
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
|
||||
- `npm run gulp vscode-darwin-x64` (Intel)
|
||||
|
||||
### Windows
|
||||
- `npm run gulp vscode-win32-x64` - most common
|
||||
- `npm run gulp vscode-win32-ia32`
|
||||
|
||||
### Linux
|
||||
- `npm run gulp vscode-linux-x64` - most common
|
||||
- `npm run gulp vscode-linux-arm`
|
||||
- `npm run gulp vscode-linux-ia32`
|
||||
|
||||
|
||||
## Roadmap
|
||||
|
||||
Here are the most important topics on our Roadmap. More ⭐'s = more important. Please refer to our [Issues](https://github.com/voideditor/void/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.
|
||||
|
||||
## ⭐⭐⭐ 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.
|
||||
|
||||
## ⭐⭐⭐ Creative.
|
||||
|
||||
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.
|
||||
|
||||
⭐ Let the user Accept / Reject all Diffs in an entire file via the sidebar.
|
||||
|
||||
# Guidelines
|
||||
|
||||
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project. Feel free to shoot us a message in the #general channel of the [Discord](https://discord.gg/RSNjgaugJs) for any reason. Please check in especially if you want to make a lot of changes or build a large new feature.
|
||||
|
||||
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
Please submit a pull request once you've made a change. You don't need to submit an issue.
|
||||
- Please submit a pull request once you've made a change. No need to submit an Issue unless you're creating a new feature.
|
||||
- Please don't use AI to write your PR 🙂.
|
||||
|
||||
Please don't use AI to write your PR 🙂.
|
||||
|
||||
<!--
|
||||
# Relevant files
|
||||
|
||||
We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
@@ -145,7 +134,7 @@ We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
- build/npm/dirs.js
|
||||
|
||||
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
|
||||
|
||||
-->
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -2,23 +2,27 @@
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
## Beginners / Getting started
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VS Code's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
## Misc
|
||||
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - sometimes useful to reference.
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
|
||||
## VSCode's Extension API
|
||||
|
||||
Void is mainly an extension right now, and these links were very useful for us to get set up.
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
This folder is no longer relevant, since Void is no longer just an extension.
|
||||
|
||||
See the new Void code in `src/vs/workbench/contrib/void` (and a few other locations - you can just search "Void").
|
||||
@@ -1,471 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage';
|
||||
import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
|
||||
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
|
||||
/*
|
||||
A summary of autotab:
|
||||
|
||||
Postprocessing
|
||||
-one common problem for all models is outputting unbalanced parentheses
|
||||
we solve this by trimming all extra closing parentheses from the generated string
|
||||
in future, should make sure parentheses are always balanced
|
||||
|
||||
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
|
||||
we complete up to first matchup character
|
||||
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
|
||||
|
||||
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
|
||||
this should happen automatically with caching system
|
||||
should break preloaded responses into \n\n chunks
|
||||
|
||||
Preprocessing
|
||||
- we don't generate if cursor is at end / beginning of a line (no spaces)
|
||||
- we generate 1 line if there is text to the right of cursor
|
||||
- we generate 1 line if variable declaration
|
||||
- (in many cases want to show 1 line but generate multiple)
|
||||
|
||||
State
|
||||
- cache based on prefix (and do some trimming first)
|
||||
- when press tab on one line, should have an immediate followup response
|
||||
to do this, show autocompletes before they're fully finished
|
||||
- [todo] remove each autotab when accepted
|
||||
- [todo] treat windows \r\n separately from \n
|
||||
!- [todo] provide type information
|
||||
|
||||
Details
|
||||
-generated results are trimmed up to 1 leading/trailing space
|
||||
-prefixes are cached up to 1 trailing newline
|
||||
-
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type AutocompletionStatus = 'pending' | 'finished' | 'error';
|
||||
type Autocompletion = {
|
||||
id: number,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
startTime: number,
|
||||
endTime: number | undefined,
|
||||
abortRef: AbortRef,
|
||||
status: AutocompletionStatus,
|
||||
llmPromise: Promise<string> | undefined,
|
||||
result: string,
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIME = 500
|
||||
const TIMEOUT_TIME = 60000
|
||||
const MAX_CACHE_SIZE = 20
|
||||
const MAX_PENDING_REQUESTS = 2
|
||||
|
||||
// postprocesses the result
|
||||
const postprocessResult = (result: string) => {
|
||||
|
||||
console.log('result: ', JSON.stringify(result))
|
||||
|
||||
// trim all whitespace except for a single leading/trailing space
|
||||
const hasLeadingSpace = result.startsWith(' ');
|
||||
const hasTrailingSpace = result.endsWith(' ');
|
||||
return (hasLeadingSpace ? ' ' : '')
|
||||
+ result.trim()
|
||||
+ (hasTrailingSpace ? ' ' : '');
|
||||
|
||||
}
|
||||
|
||||
const extractCodeFromResult = (result: string) => {
|
||||
|
||||
// extract the code between triple backticks
|
||||
const parts = result.split(/```(?:\s*\w+)?\n?/);
|
||||
|
||||
// if there is no ``` then return the raw result
|
||||
if (parts.length === 1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// else return the code between the triple backticks
|
||||
return parts[1]
|
||||
|
||||
}
|
||||
|
||||
// trims the end of the prefix to improve cache hit rate
|
||||
const trimPrefix = (prefix: string) => {
|
||||
const trimmedPrefix = prefix.trimEnd()
|
||||
const trailingEnd = prefix.substring(trimmedPrefix.length)
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes('\n')) {
|
||||
return trimmedPrefix + '\n'
|
||||
}
|
||||
|
||||
// else ignore all spaces and return the trimmed prefix
|
||||
return trimmedPrefix
|
||||
}
|
||||
|
||||
function getStringUpToUnbalancedParenthesis(s: string, prefixToTheLeft: string): string {
|
||||
|
||||
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
|
||||
|
||||
// todo find first open bracket in prefix and get all brackets beyond it in prefix
|
||||
// get all bracets in prefix
|
||||
let stack: string[] = []
|
||||
const firstOpenIdx = prefixToTheLeft.search(/[[({]/);
|
||||
if (firstOpenIdx !== -1) stack = prefixToTheLeft.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c))
|
||||
|
||||
// Iterate through each character
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const char = s[i];
|
||||
|
||||
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
|
||||
else if (char === ')' || char === '}' || char === ']') {
|
||||
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// finds the text in the autocompletion to display, assuming the prefix is already matched
|
||||
// example:
|
||||
// originalPrefix = abcd
|
||||
// generatedMiddle = efgh
|
||||
// originalSuffix = ijkl
|
||||
// the user has typed "ef" so prefix = abcdef
|
||||
// we want to return the rest of the generatedMiddle, which is "gh"
|
||||
const toInlineCompletion = ({ prefix, suffix, autocompletion, position }: { prefix: string, suffix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => {
|
||||
const originalPrefix = autocompletion.prefix
|
||||
const generatedMiddle = autocompletion.result
|
||||
|
||||
const trimmedOriginalPrefix = trimPrefix(originalPrefix)
|
||||
const trimmedCurrentPrefix = trimPrefix(prefix)
|
||||
|
||||
const suffixLines = suffix.split('\n')
|
||||
const prefixLines = trimmedCurrentPrefix.split('\n')
|
||||
const suffixToTheRightOfCursor = suffixLines[0].trim()
|
||||
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1].trim()
|
||||
|
||||
const generatedLines = generatedMiddle.split('\n')
|
||||
|
||||
// compute startIdx
|
||||
let startIdx = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length
|
||||
if (startIdx < 0) {
|
||||
return new vscode.InlineCompletionItem('')
|
||||
}
|
||||
|
||||
// compute endIdx
|
||||
// hacks to get the suffix to render properly with lower quality models
|
||||
// if the generated text matches with the suffix on the current line, stop
|
||||
let endIdx: number | undefined = generatedMiddle.length // exclusive bounds
|
||||
|
||||
if (suffixToTheRightOfCursor !== '') { // completing in the middle of a line
|
||||
console.log('1')
|
||||
// complete until there is a match
|
||||
const matchIndex = generatedMiddle.lastIndexOf(suffixToTheRightOfCursor[0])
|
||||
if (matchIndex > 0) { endIdx = matchIndex }
|
||||
}
|
||||
|
||||
if (prefixToTheLeftOfCursor !== '') { // completing the end of a line
|
||||
console.log('2')
|
||||
// show a single line
|
||||
const newlineIdx = generatedMiddle.indexOf('\n')
|
||||
if (newlineIdx > -1) { endIdx = newlineIdx }
|
||||
}
|
||||
|
||||
// // if a generated line matches with a suffix line, stop
|
||||
// if (suffixLines.length > 1) {
|
||||
// console.log('3')
|
||||
// const lines = []
|
||||
// for (const generatedLine of generatedLines) {
|
||||
// if (suffixLines.slice(0, 10).some(suffixLine =>
|
||||
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
|
||||
// && generatedLine.trim().startsWith(suffixLine.trim())
|
||||
// )) break;
|
||||
// lines.push(generatedLine)
|
||||
// }
|
||||
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
|
||||
// }
|
||||
|
||||
let completionStr = generatedMiddle.slice(startIdx, endIdx)
|
||||
|
||||
// filter out unbalanced parentheses
|
||||
console.log('completionStrBeforeParens: ', JSON.stringify(completionStr))
|
||||
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefixLines.slice(-2).join('\n'))
|
||||
|
||||
console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
|
||||
console.log('finalCompletionStr: ', JSON.stringify(completionStr))
|
||||
|
||||
return new vscode.InlineCompletionItem(completionStr, new vscode.Range(position, position))
|
||||
|
||||
}
|
||||
|
||||
// returns whether this autocompletion is in the cache
|
||||
const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
|
||||
|
||||
const originalPrefix = autocompletion.prefix
|
||||
const generatedMiddle = autocompletion.result
|
||||
const originalPrefixTrimmed = trimPrefix(originalPrefix)
|
||||
const currentPrefixTrimmed = trimPrefix(prefix)
|
||||
|
||||
if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed)
|
||||
return isMatch
|
||||
|
||||
}
|
||||
|
||||
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
|
||||
|
||||
const prefixLines = prefix.split('\n')
|
||||
const suffixLines = suffix.split('\n')
|
||||
|
||||
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
|
||||
const suffixToRightOfCursor = suffixLines[0]
|
||||
|
||||
// default parameters
|
||||
let shouldGenerate = true
|
||||
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
|
||||
|
||||
// specific cases
|
||||
if (suffixToRightOfCursor.trim() !== '') { // typing between something
|
||||
stopTokens = ['\n', '\r\n']
|
||||
}
|
||||
|
||||
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
|
||||
// stopTokens = ['\n\n', '\r\n\r\n']
|
||||
// }
|
||||
|
||||
if (prefixToLeftOfCursor === '' || suffixToRightOfCursor === '') { // at beginning or end of line
|
||||
shouldGenerate = false
|
||||
}
|
||||
|
||||
console.log('shouldGenerate:', shouldGenerate, stopTokens)
|
||||
|
||||
return { shouldGenerate, stopTokens }
|
||||
|
||||
}
|
||||
|
||||
export class AutocompleteProvider implements vscode.InlineCompletionItemProvider {
|
||||
|
||||
private _extensionContext: vscode.ExtensionContext;
|
||||
|
||||
private _autocompletionId: number = 0;
|
||||
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
|
||||
|
||||
private _lastCompletionTime = 0
|
||||
private _lastPrefix: string = ''
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._extensionContext = context
|
||||
}
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke and returns the completion to show
|
||||
async provideInlineCompletionItems(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
context: vscode.InlineCompletionContext,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<vscode.InlineCompletionItem[]> {
|
||||
|
||||
const disabled = false
|
||||
if (disabled) { return []; }
|
||||
|
||||
const docUriStr = document.uri.toString()
|
||||
|
||||
|
||||
const fullText = document.getText();
|
||||
const cursorOffset = document.offsetAt(position);
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
const suffix = fullText.substring(cursorOffset)
|
||||
const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
// initialize cache and other variables
|
||||
// note that whenever an autocompletion is rejected, it is removed from cache
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) {
|
||||
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>({
|
||||
max: MAX_CACHE_SIZE,
|
||||
dispose: (autocompletion) => {
|
||||
autocompletion.abortRef.current()
|
||||
}
|
||||
})
|
||||
}
|
||||
this._lastPrefix = prefix
|
||||
|
||||
// get all pending autocompletions
|
||||
let __c = 0
|
||||
this._autocompletionsOfDocument[docUriStr].forEach(a => { if (a.status === 'pending') __c += 1 })
|
||||
console.log('pending: ' + __c)
|
||||
|
||||
// get autocompletion from cache
|
||||
let cachedAutocompletion: Autocompletion | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
|
||||
// if the user's change matches up with the generated text
|
||||
if (doesPrefixMatchAutocompletion({ prefix, autocompletion })) {
|
||||
cachedAutocompletion = autocompletion
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a cached autocompletion, return it
|
||||
if (cachedAutocompletion) {
|
||||
|
||||
if (cachedAutocompletion.status === 'finished') {
|
||||
console.log('A1')
|
||||
|
||||
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} else if (cachedAutocompletion.status === 'pending') {
|
||||
console.log('A2')
|
||||
|
||||
try {
|
||||
await cachedAutocompletion.llmPromise;
|
||||
console.log('id: ' + cachedAutocompletion.id)
|
||||
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id)
|
||||
console.error('Error creating autocompletion (1): ' + e)
|
||||
}
|
||||
|
||||
} else if (cachedAutocompletion.status === 'error') {
|
||||
console.log('A3')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// else if no more typing happens, then go forwards with the request
|
||||
// wait DEBOUNCE_TIME for the user to stop typing
|
||||
const thisTime = Date.now()
|
||||
this._lastCompletionTime = thisTime
|
||||
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this._lastCompletionTime === thisTime) {
|
||||
resolve(false)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
}, DEBOUNCE_TIME)
|
||||
)
|
||||
|
||||
// if more typing happened, then do not go forwards with the request
|
||||
if (didTypingHappenDuringDebounce) {
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('B')
|
||||
|
||||
// if there are too many pending requests, cancel the oldest one
|
||||
let numPending = 0
|
||||
let oldestPending: Autocompletion | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
|
||||
if (autocompletion.status === 'pending') {
|
||||
numPending += 1
|
||||
if (oldestPending === undefined) {
|
||||
oldestPending = autocompletion
|
||||
}
|
||||
if (numPending >= MAX_PENDING_REQUESTS) {
|
||||
// cancel the oldest pending request and remove it from cache
|
||||
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix })
|
||||
|
||||
if (!shouldGenerate) return []
|
||||
|
||||
// create a new autocompletion and add it to cache
|
||||
const newAutocompletion: Autocompletion = {
|
||||
id: this._autocompletionId++,
|
||||
prefix: prefix,
|
||||
suffix: suffix,
|
||||
startTime: Date.now(),
|
||||
endTime: undefined,
|
||||
abortRef: { current: () => { } },
|
||||
status: 'pending',
|
||||
llmPromise: undefined,
|
||||
result: '',
|
||||
}
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
sendLLMMessage({
|
||||
mode: 'fim',
|
||||
fimInfo: { prefix, suffix },
|
||||
options: { stopTokens },
|
||||
onText: async (tokenStr, completionStr) => {
|
||||
|
||||
newAutocompletion.result = completionStr
|
||||
|
||||
// if generation doesn't match the prefix for the first few tokens generated, reject it
|
||||
if (!doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
reject('LLM response did not match user\'s text.')
|
||||
}
|
||||
},
|
||||
onFinalMessage: (finalMessage) => {
|
||||
|
||||
// newAutocompletion.prefix = prefix
|
||||
// newAutocompletion.suffix = suffix
|
||||
// newAutocompletion.startTime = Date.now()
|
||||
newAutocompletion.endTime = Date.now()
|
||||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
newAutocompletion.result = postprocessResult(extractCodeFromResult(finalMessage))
|
||||
|
||||
resolve(newAutocompletion.result)
|
||||
|
||||
},
|
||||
onError: (e) => {
|
||||
newAutocompletion.endTime = Date.now()
|
||||
newAutocompletion.status = 'error'
|
||||
reject(e)
|
||||
},
|
||||
voidConfig,
|
||||
abortRef: newAutocompletion.abortRef,
|
||||
})
|
||||
|
||||
// if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
|
||||
setTimeout(() => {
|
||||
if (newAutocompletion.status === 'pending') {
|
||||
reject('Timeout receiving message to LLM.')
|
||||
}
|
||||
}, TIMEOUT_TIME)
|
||||
|
||||
|
||||
})
|
||||
|
||||
// add autocompletion to cache
|
||||
this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion)
|
||||
|
||||
// show autocompletion
|
||||
try {
|
||||
await newAutocompletion.llmPromise
|
||||
console.log('id: ' + newAutocompletion.id)
|
||||
|
||||
const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
|
||||
console.error('Error creating autocompletion (2): ' + e)
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Generated
+4291
-3913
File diff suppressed because it is too large
Load Diff
+18
-10
@@ -9,6 +9,8 @@
|
||||
"main": "./out/main",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
|
||||
"watchreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js --watch && cd ../../../../../../../",
|
||||
"test": "echo Please run any of the test scripts from the scripts folder.",
|
||||
"test-browser": "npx playwright install && node test/unit/browser/index.js",
|
||||
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
|
||||
@@ -72,9 +74,13 @@
|
||||
"update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@parcel/watcher": "2.1.0",
|
||||
"@rrweb/record": "^2.0.0-alpha.17",
|
||||
"@rrweb/types": "^2.0.0-alpha.17",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.0",
|
||||
"@vscode/policy-watcher": "^1.1.4",
|
||||
@@ -97,6 +103,9 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.64",
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"ajv": "^8.17.1",
|
||||
"diff": "^7.0.0",
|
||||
"groq-sdk": "^0.9.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.3",
|
||||
@@ -107,7 +116,13 @@
|
||||
"native-keymap": "^3.3.5",
|
||||
"native-watchdog": "^1.4.1",
|
||||
"node-pty": "1.1.0-beta21",
|
||||
"ollama": "^0.5.11",
|
||||
"open": "^8.4.2",
|
||||
"openai": "^4.76.1",
|
||||
"posthog-node": "^4.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-oniguruma": "1.7.0",
|
||||
@@ -117,13 +132,12 @@
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@swc/core": "1.3.62",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/gulp-svgmin": "^1.2.1",
|
||||
"@types/http-proxy-agent": "^2.0.1",
|
||||
"@types/kerberos": "^1.1.2",
|
||||
@@ -164,7 +178,6 @@
|
||||
"cssnano": "^6.0.3",
|
||||
"debounce": "^1.0.0",
|
||||
"deemon": "^1.8.0",
|
||||
"diff": "^7.0.0",
|
||||
"electron": "30.5.1",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
@@ -207,9 +220,8 @@
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"nodemon": "^3.1.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ollama": "^0.5.9",
|
||||
"openai": "^4.71.1",
|
||||
"opn": "^6.0.0",
|
||||
"original-fs": "^1.2.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
@@ -217,14 +229,10 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-nesting": "^12.0.2",
|
||||
"posthog-js": "^1.184.2",
|
||||
"pump": "^1.0.1",
|
||||
"rcedit": "^1.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.1",
|
||||
"scope-tailwind": "^1.0.5",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-test": "^3.1.3",
|
||||
"source-map": "0.6.1",
|
||||
|
||||
+5
-50
@@ -31,54 +31,9 @@
|
||||
"nodejsRepository": "https://nodejs.org",
|
||||
"urlProtocol": "code-oss",
|
||||
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
|
||||
"builtInExtensions": [
|
||||
{
|
||||
"name": "ms-vscode.js-debug-companion",
|
||||
"version": "1.1.3",
|
||||
"sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93",
|
||||
"repo": "https://github.com/microsoft/vscode-js-debug-companion",
|
||||
"metadata": {
|
||||
"id": "99cb0b7f-7354-4278-b8da-6cc79972169d",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.js-debug",
|
||||
"version": "1.93.0",
|
||||
"sha256": "9339cb8e6b77f554df54d79e71f533279cb76b0f9b04c207f633bfd507442b6a",
|
||||
"repo": "https://github.com/microsoft/vscode-js-debug",
|
||||
"metadata": {
|
||||
"id": "25629058-ddac-4e17-abba-74678e126c5d",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.vscode-js-profile-table",
|
||||
"version": "1.0.9",
|
||||
"sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6",
|
||||
"repo": "https://github.com/microsoft/vscode-js-profile-visualizer",
|
||||
"metadata": {
|
||||
"id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
}
|
||||
]
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://open-vsx.org/vscode/gallery",
|
||||
"itemUrl": "https://open-vsx.org/vscode/item"
|
||||
},
|
||||
"builtInExtensions": []
|
||||
}
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ protocol.registerSchemesAsPrivileged([
|
||||
},
|
||||
{
|
||||
scheme: 'vscode-file',
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true }
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true, }
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
+2
-2
@@ -52,10 +52,10 @@
|
||||
"./typings",
|
||||
"./vs/**/*.ts",
|
||||
"vscode-dts/vscode.proposed.*.d.ts",
|
||||
"vscode-dts/vscode.d.ts"
|
||||
"vscode-dts/vscode.d.ts",
|
||||
|
||||
// Void added these:
|
||||
// "./vs/**/*.tsx",
|
||||
// "./vs/workbench/contrib/void/browser/**.tsx",
|
||||
// "./vs/**/*.d.mts",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -122,6 +122,8 @@ import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cs
|
||||
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
|
||||
|
||||
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
|
||||
import { IMetricsService } from '../../platform/void/common/metricsService.js';
|
||||
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
|
||||
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
@@ -1103,6 +1105,9 @@ export class CodeApplication extends Disposable {
|
||||
services.set(ITelemetryService, NullTelemetryService);
|
||||
}
|
||||
|
||||
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
|
||||
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
|
||||
@@ -1237,10 +1242,11 @@ export class CodeApplication extends Disposable {
|
||||
mainProcessElectronServer.registerChannel('logger', loggerChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
|
||||
|
||||
// Void
|
||||
// const sendLLMMessageChannel = ProxyChannel.fromService(accessor.get(ISendLLMMessageService), disposables);
|
||||
const sendLLMMessageChannel = new LLMMessageChannel();
|
||||
mainProcessElectronServer.registerChannel('void-channel-sendLLMMessage', sendLLMMessageChannel);
|
||||
// Void - use loggerChannel as reference
|
||||
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
|
||||
const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel);
|
||||
|
||||
// Extension Host Debug Broadcasting
|
||||
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
|
||||
import { IRange } from '../../../common/core/range.js';
|
||||
import { EditorOption } from '../../../common/config/editorOptions.js';
|
||||
|
||||
|
||||
// THIS FILE IS OLD!!!
|
||||
export interface IInlineDiffService {
|
||||
readonly _serviceBrand: undefined;
|
||||
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ExpandLineSelectionAction extends EditorAction {
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorCore,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyM // Void changed this to Cmd+M
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, LLMMessageServiceParams, ProxyLLMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { IDisposable } from '../../../base/common/lifecycle.js';
|
||||
|
||||
|
||||
// BROWSER IMPLEMENTATION OF SENDLLMMESSAGE
|
||||
export const ISendLLMMessageService = createDecorator<ISendLLMMessageService>('sendLLMMessageService');
|
||||
|
||||
// defines an interface that node/ creates and browser/ uses
|
||||
export interface ISendLLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
sendLLMMessage: (params: LLMMessageServiceParams) => string;
|
||||
abort: (requestId: string) => void;
|
||||
}
|
||||
|
||||
|
||||
export class SendLLMMessageService implements ISendLLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel;
|
||||
|
||||
private readonly _disposablesOfRequestId: Record<string, IDisposable[]> = {}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side)
|
||||
) {
|
||||
|
||||
this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage')
|
||||
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service, not needed here
|
||||
}
|
||||
|
||||
_addDisposable(requestId: string, disposable: IDisposable) {
|
||||
if (!this._disposablesOfRequestId[requestId]) {
|
||||
this._disposablesOfRequestId[requestId] = []
|
||||
}
|
||||
this._disposablesOfRequestId[requestId].push(disposable)
|
||||
}
|
||||
|
||||
|
||||
|
||||
sendLLMMessage(params: LLMMessageServiceParams) {
|
||||
const requestId_ = generateUuid();
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
|
||||
// listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it
|
||||
|
||||
const onTextEvent: Event<ProxyOnTextPayload> = this.channel.listen('onText')
|
||||
this._addDisposable(requestId_,
|
||||
onTextEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
onText(e)
|
||||
})
|
||||
)
|
||||
|
||||
const onFinalMessageEvent: Event<ProxyOnFinalMessagePayload> = this.channel.listen('onFinalMessage')
|
||||
this._addDisposable(requestId_,
|
||||
onFinalMessageEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
onFinalMessage(e)
|
||||
this._dispose(requestId_)
|
||||
})
|
||||
)
|
||||
|
||||
const onErrorEvent: Event<ProxyOnErrorPayload> = this.channel.listen('onError')
|
||||
this._addDisposable(requestId_,
|
||||
onErrorEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
console.log('event onError', JSON.stringify(e))
|
||||
onError(e)
|
||||
this._dispose(requestId_)
|
||||
})
|
||||
)
|
||||
|
||||
// params will be stripped of all its functions
|
||||
this.channel.call('sendLLMMessage', { ...proxyParams, requestId: requestId_ } satisfies ProxyLLMMessageParams);
|
||||
|
||||
return requestId_
|
||||
}
|
||||
|
||||
private _dispose(requestId: string) {
|
||||
if (!(requestId in this._disposablesOfRequestId)) return
|
||||
for (const disposable of this._disposablesOfRequestId[requestId]) {
|
||||
disposable.dispose()
|
||||
}
|
||||
delete this._disposablesOfRequestId[requestId]
|
||||
}
|
||||
|
||||
abort(requestId: string) {
|
||||
this.channel.call('abort', { requestId } satisfies ProxyLLMMessageAbortParams);
|
||||
this._dispose(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
// ---------- common ----------
|
||||
|
||||
// llmMessage
|
||||
import '../common/llmMessageService.js'
|
||||
|
||||
// voidSettings
|
||||
import '../common/voidSettingsService.js'
|
||||
|
||||
// refreshModel
|
||||
import '../common/refreshModelService.js'
|
||||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
@@ -0,0 +1,174 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
// import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
||||
// calls channel to implement features
|
||||
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
||||
|
||||
export interface ILLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => void;
|
||||
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
||||
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
|
||||
}
|
||||
|
||||
export class LLMMessageService extends Disposable implements ILLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel // LLMMessageChannel
|
||||
|
||||
// llmMessage
|
||||
private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {}
|
||||
private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {}
|
||||
private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {}
|
||||
|
||||
|
||||
// ollamaList
|
||||
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) } = {}
|
||||
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
|
||||
|
||||
// openAICompatibleList
|
||||
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
// @INotificationService private readonly notificationService: INotificationService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
|
||||
|
||||
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
||||
// llm
|
||||
this._register((this.channel.listen('onText_llm') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||
this.onTextHooks_llm[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onFinalMessage_llm') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||
this.onFinalMessageHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
console.log('Error in LLMMessageService:', JSON.stringify(e))
|
||||
this.onErrorHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
// ollama
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
// openaiCompatible
|
||||
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onSuccess_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onError_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
sendLLMMessage(params: ServiceSendLLMMessageParams) {
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
const { featureName } = proxyParams
|
||||
|
||||
// end early if no provider
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
if (modelSelection === null) {
|
||||
onError({ message: 'Please add a Provider in Settings!', fullError: null })
|
||||
return null
|
||||
}
|
||||
const { providerName, modelName } = modelSelection
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onTextHooks_llm[requestId_] = onText
|
||||
this.onFinalMessageHooks_llm[requestId_] = onFinalMessage
|
||||
this.onErrorHooks_llm[requestId_] = onError
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// params will be stripped of all its functions over the IPC channel
|
||||
this.channel.call('sendLLMMessage', {
|
||||
...proxyParams,
|
||||
requestId: requestId_,
|
||||
providerName,
|
||||
modelName,
|
||||
settingsOfProvider,
|
||||
} satisfies MainLLMMessageParams);
|
||||
|
||||
return requestId_
|
||||
}
|
||||
|
||||
|
||||
abort(requestId: string) {
|
||||
this.channel.call('abort', { requestId } satisfies MainLLMMessageAbortParams);
|
||||
this._onRequestIdDone(requestId)
|
||||
}
|
||||
|
||||
|
||||
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_ollama[requestId_] = onSuccess
|
||||
this.onError_ollama[requestId_] = onError
|
||||
|
||||
this.channel.call('ollamaList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OllamaModelResponse>)
|
||||
}
|
||||
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_openAICompatible[requestId_] = onSuccess
|
||||
this.onError_openAICompatible[requestId_] = onError
|
||||
|
||||
this.channel.call('openAICompatibleList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_onRequestIdDone(requestId: string) {
|
||||
delete this.onTextHooks_llm[requestId]
|
||||
delete this.onFinalMessageHooks_llm[requestId]
|
||||
delete this.onErrorHooks_llm[requestId]
|
||||
|
||||
delete this.onSuccess_ollama[requestId]
|
||||
delete this.onError_ollama[requestId]
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VoidConfig } from '../../../workbench/contrib/void/browser/registerConfig.js';
|
||||
import { IRange } from '../../../editor/common/core/range'
|
||||
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
// ---------- type definitions ----------
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
|
||||
export type OnFinalMessage = (p: { fullText: string }) => void
|
||||
|
||||
export type OnError = (p: { error: Error | string }) => void
|
||||
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type LLMMessage = {
|
||||
@@ -20,39 +17,133 @@ export type LLMMessage = {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type LLMMessageServiceParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
voidConfig: VoidConfig | null;
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
export type ServiceSendLLMFeatureParams = {
|
||||
featureName: 'Ctrl+K';
|
||||
range: IRange;
|
||||
} | {
|
||||
featureName: 'Ctrl+L';
|
||||
} | {
|
||||
featureName: 'Autocomplete';
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
export type SendLLMMMessageParams = {
|
||||
// params to the true sendLLMMessage function
|
||||
export type LLMMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
voidConfig: VoidConfig | null;
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
abortRef: AbortRef;
|
||||
|
||||
messages: LLMMessage[];
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
providerName: ProviderName;
|
||||
modelName: string;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
}
|
||||
|
||||
export type ServiceSendLLMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
} & ServiceSendLLMFeatureParams
|
||||
|
||||
// can't send functions across a proxy, use listeners instead
|
||||
export const listenerNames = ['onText', 'onFinalMessage', 'onError'] as const
|
||||
export type ProxyLLMMessageParams = Omit<LLMMessageServiceParams, typeof listenerNames[number]> & { requestId: string }
|
||||
export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
|
||||
|
||||
export type ProxyOnTextPayload = Parameters<OnText>[0] & { requestId: string }
|
||||
export type ProxyOnFinalMessagePayload = Parameters<OnFinalMessage>[0] & { requestId: string }
|
||||
export type ProxyOnErrorPayload = Parameters<OnError>[0] & { requestId: string }
|
||||
export type MainLLMMessageParams = Omit<LLMMMessageParams, BlockedMainLLMMessageParams> & { requestId: string }
|
||||
export type MainLLMMessageAbortParams = { requestId: string }
|
||||
|
||||
export type ProxyLLMMessageAbortParams = { requestId: string }
|
||||
export type EventLLMMessageOnTextParams = Parameters<OnText>[0] & { requestId: string }
|
||||
export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0] & { requestId: string }
|
||||
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
|
||||
|
||||
export type _InternalSendLLMMessageFnType = (params: {
|
||||
messages: LLMMessage[];
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
providerName: ProviderName;
|
||||
modelName: string;
|
||||
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}) => void
|
||||
|
||||
// service -> main -> internal -> event (back to main)
|
||||
// (browser)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// These are from 'ollama' SDK
|
||||
interface OllamaModelDetails {
|
||||
parent_model: string;
|
||||
format: string;
|
||||
family: string;
|
||||
families: string[];
|
||||
parameter_size: string;
|
||||
quantization_level: string;
|
||||
}
|
||||
|
||||
export type OllamaModelResponse = {
|
||||
name: string;
|
||||
modified_at: Date;
|
||||
size: number;
|
||||
digest: string;
|
||||
details: OllamaModelDetails;
|
||||
expires_at: Date;
|
||||
size_vram: number;
|
||||
}
|
||||
|
||||
export type OpenaiCompatibleModelResponse = {
|
||||
id: string;
|
||||
created: number;
|
||||
object: 'model';
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
export type ModelListParams<modelResponse> = {
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onError: (param: { error: string }) => void;
|
||||
}
|
||||
|
||||
// params to the service
|
||||
export type ServiceModelListParams<modelResponse> = {
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onError: (param: { error: any }) => void;
|
||||
}
|
||||
|
||||
type BlockedMainModelListParams = 'onSuccess' | 'onError'
|
||||
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
|
||||
|
||||
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
|
||||
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
|
||||
|
||||
|
||||
|
||||
|
||||
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
}
|
||||
|
||||
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
|
||||
|
||||
|
||||
// implemented by calling channel
|
||||
export class MetricsService implements IMetricsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly metricsService: IMetricsService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
|
||||
) {
|
||||
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
|
||||
}
|
||||
|
||||
// call capture on the channel
|
||||
capture(...params: Parameters<IMetricsService['capture']>) {
|
||||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { ILLMMessageService } from './llmMessageService.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
|
||||
|
||||
|
||||
|
||||
|
||||
type RefreshableState = ({
|
||||
state: 'init',
|
||||
timeoutId: null,
|
||||
} | {
|
||||
state: 'refreshing',
|
||||
timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels
|
||||
} | {
|
||||
state: 'success',
|
||||
timeoutId: null,
|
||||
})
|
||||
|
||||
|
||||
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
|
||||
|
||||
|
||||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_enabled', 'endpoint'],
|
||||
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
|
||||
// element-wise equals
|
||||
function eq<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
export interface IRefreshModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
|
||||
onDidChangeState: Event<RefreshableProviderName>;
|
||||
state: RefreshModelStateOfProvider;
|
||||
}
|
||||
|
||||
export const IRefreshModelService = createDecorator<IRefreshModelService>('RefreshModelService');
|
||||
|
||||
export class RefreshModelService extends Disposable implements IRefreshModelService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<RefreshableProviderName>();
|
||||
readonly onDidChangeState: Event<RefreshableProviderName> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
|
||||
const disposables: Set<IDisposable> = new Set()
|
||||
|
||||
const initializePollingAndOnChange = () => {
|
||||
this._clearAllTimeouts()
|
||||
disposables.forEach(d => d.dispose())
|
||||
disposables.clear()
|
||||
|
||||
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
|
||||
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.refreshModels(providerName, !enabled)
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
|
||||
disposables.add(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
|
||||
const prevEnabled = prevVals[0] as boolean
|
||||
const enabled = newVals[0] as boolean
|
||||
|
||||
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
|
||||
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
|
||||
// if user just clicked enable, refresh
|
||||
this.refreshModels(providerName, !enabled)
|
||||
}
|
||||
else {
|
||||
// else if user just clicked disable, don't refresh
|
||||
|
||||
// //give cooldown before re-enabling (or at least re-fetching)
|
||||
// const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT)
|
||||
// this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
prevVals = newVals
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models
|
||||
voidSettingsService.waitForInitState.then(() => {
|
||||
initializePollingAndOnChange()
|
||||
this._register(
|
||||
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
openAICompatible: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
// start listening for models (and don't stop until success)
|
||||
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean) {
|
||||
this._clearProviderTimeout(providerName)
|
||||
|
||||
// start loading models
|
||||
this._setRefreshState(providerName, 'refreshing')
|
||||
|
||||
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
: () => { }
|
||||
|
||||
fn({
|
||||
onSuccess: ({ models }) => {
|
||||
this.voidSettingsService.setDefaultModels(providerName, models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name
|
||||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName)
|
||||
}))
|
||||
|
||||
if (enableProviderOnSuccess) {
|
||||
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
}
|
||||
|
||||
this._setRefreshState(providerName, 'success')
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
// poll
|
||||
console.log('retrying list models:', providerName, error)
|
||||
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_clearAllTimeouts() {
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
this._clearProviderTimeout(providerName)
|
||||
}
|
||||
}
|
||||
|
||||
_clearProviderTimeout(providerName: RefreshableProviderName) {
|
||||
// cancel any existing poll
|
||||
if (this.state[providerName].timeoutId) {
|
||||
clearTimeout(this.state[providerName].timeoutId)
|
||||
this._setTimeoutId(providerName, null)
|
||||
}
|
||||
}
|
||||
|
||||
private _setTimeoutId(providerName: RefreshableProviderName, timeoutId: NodeJS.Timeout | null) {
|
||||
this.state[providerName].timeoutId = timeoutId
|
||||
}
|
||||
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
|
||||
this.state[providerName].state = state
|
||||
this._onDidChangeState.fire(providerName)
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IRefreshModelService, RefreshModelService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../base/common/objects.js';
|
||||
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.voidSettingsStorage'
|
||||
|
||||
type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
providerName: ProviderName,
|
||||
settingName: S,
|
||||
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
|
||||
) => Promise<void>;
|
||||
|
||||
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
||||
featureName: K,
|
||||
newVal: ModelSelectionOfFeature[K],
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
|
||||
|
||||
export type ModelOption = { text: string, value: ModelSelection }
|
||||
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly featureFlagSettings: FeatureFlagSettings;
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
onDidChangeState: Event<EventProp>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setFeatureFlag: SetFeatureFlagFn;
|
||||
|
||||
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
}
|
||||
|
||||
|
||||
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
|
||||
let modelOptions: ModelOption[] = []
|
||||
for (const providerName of providerNames) {
|
||||
const providerConfig = settingsOfProvider[providerName]
|
||||
if (!providerConfig._enabled) continue // if disabled, don't display model options
|
||||
for (const { modelName, isHidden } of providerConfig.models) {
|
||||
if (isHidden) continue
|
||||
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
|
||||
}
|
||||
}
|
||||
return modelOptions
|
||||
}
|
||||
|
||||
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
|
||||
export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidSettingsService');
|
||||
class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<EventProp>();
|
||||
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
waitForInitState: Promise<void> // await this if you need a valid state initially
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IEncryptionService private readonly _encryptionService: IEncryptionService,
|
||||
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
|
||||
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// at the start, we haven't read the partial config yet, but we need to set state to something
|
||||
this.state = defaultState()
|
||||
|
||||
let resolver: () => void = () => { }
|
||||
this.waitForInitState = new Promise((res, rej) => resolver = res)
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(s => {
|
||||
this.state = s
|
||||
resolver()
|
||||
this._onDidChangeState.fire('all')
|
||||
})
|
||||
}
|
||||
|
||||
private async _readState(): Promise<VoidSettingsState> {
|
||||
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedState)
|
||||
return defaultState()
|
||||
|
||||
const stateStr = await this._encryptionService.decrypt(encryptedState)
|
||||
return JSON.parse(stateStr)
|
||||
}
|
||||
|
||||
|
||||
private async _storeState() {
|
||||
const state = this.state
|
||||
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
|
||||
this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => {
|
||||
|
||||
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
|
||||
|
||||
const newSettingsOfProvider = {
|
||||
...this.state.settingsOfProvider,
|
||||
[providerName]: {
|
||||
...this.state.settingsOfProvider[providerName],
|
||||
[settingName]: newVal,
|
||||
}
|
||||
}
|
||||
|
||||
const newFeatureFlags = this.state.featureFlagSettings
|
||||
|
||||
// if changed models or enabled a provider, recompute models list
|
||||
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
|
||||
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
|
||||
|
||||
const newState: VoidSettingsState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
featureFlagSettings: newFeatureFlags,
|
||||
_modelOptions: newModelsList,
|
||||
}
|
||||
|
||||
// this must go above this.setanythingelse()
|
||||
this.state = newState
|
||||
|
||||
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
|
||||
if (modelsListChanged) {
|
||||
for (const featureName of featureNames) {
|
||||
|
||||
const currentSelection = newModelSelectionOfFeature[featureName]
|
||||
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection))
|
||||
|
||||
if (selnIdx === -1) {
|
||||
if (newModelsList.length !== 0)
|
||||
this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true })
|
||||
else
|
||||
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('settingsOfProvider')
|
||||
}
|
||||
|
||||
|
||||
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
|
||||
const newState = {
|
||||
...this.state,
|
||||
featureFlagSettings: {
|
||||
...this.state.featureFlagSettings,
|
||||
[flagName]: newVal
|
||||
}
|
||||
}
|
||||
this.state = newState
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('featureFlagSettings')
|
||||
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
modelSelectionOfFeature: {
|
||||
...this.state.modelSelectionOfFeature,
|
||||
[featureName]: newVal
|
||||
}
|
||||
}
|
||||
|
||||
this.state = newState
|
||||
|
||||
if (options?.doNotApplyEffects)
|
||||
return
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('modelSelectionOfFeature')
|
||||
}
|
||||
|
||||
|
||||
|
||||
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames)
|
||||
const newModels = [
|
||||
...newDefaultModels,
|
||||
...models.filter(m => !m.isDefault), // keep any non-default models
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
}
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const modelIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (modelIdx === -1) return
|
||||
const newModels: VoidModelInfo[] = [
|
||||
...models.slice(0, modelIdx),
|
||||
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
|
||||
...models.slice(modelIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
}
|
||||
addModel(providerName: ProviderName, modelName: string) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const existingIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
}
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const delIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (delIdx === -1) return false
|
||||
const newModels = [
|
||||
...models.slice(0, delIdx), // delete the idx
|
||||
...models.slice(delIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,394 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
|
||||
export type VoidModelInfo = {
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
isHidden: boolean, // whether or not the user is hiding it
|
||||
}
|
||||
|
||||
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[]): VoidModelInfo[] => {
|
||||
const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden }))
|
||||
}
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export const defaultAnthropicModels = modelInfoOfDefaultNames([
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229',
|
||||
'claude-3-sonnet-20240229',
|
||||
// 'claude-3-haiku-20240307',
|
||||
])
|
||||
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultOpenAIModels = modelInfoOfDefaultNames([
|
||||
'o1-preview',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
// 'gpt-4o-2024-05-13',
|
||||
// 'gpt-4o-2024-08-06',
|
||||
// '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',
|
||||
])
|
||||
|
||||
|
||||
|
||||
// https://console.groq.com/docs/models
|
||||
export const defaultGroqModels = modelInfoOfDefaultNames([
|
||||
"mixtral-8x7b-32768",
|
||||
"llama2-70b-4096",
|
||||
"gemma-7b-it"
|
||||
])
|
||||
|
||||
|
||||
export const defaultGeminiModels = modelInfoOfDefaultNames([
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-1.0-pro'
|
||||
])
|
||||
|
||||
|
||||
|
||||
// export const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
export const anthropicMaxPossibleTokens = (modelName: string) => {
|
||||
if (modelName === 'claude-3-5-sonnet-20241022'
|
||||
|| modelName === 'claude-3-5-haiku-20241022')
|
||||
return 8192
|
||||
if (modelName === 'claude-3-opus-20240229'
|
||||
|| modelName === 'claude-3-sonnet-20240229'
|
||||
|| modelName === 'claude-3-haiku-20240307')
|
||||
return 4096
|
||||
return 1024 // return a reasonably small number if they're using a different model
|
||||
}
|
||||
|
||||
|
||||
type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
|
||||
|
||||
export const defaultProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAI: {
|
||||
apiKey: '',
|
||||
},
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
openRouter: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAICompatible: {
|
||||
endpoint: '',
|
||||
apiKey: '',
|
||||
},
|
||||
gemini: {
|
||||
apiKey: '',
|
||||
},
|
||||
groq: {
|
||||
apiKey: ''
|
||||
}
|
||||
} as const
|
||||
|
||||
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
type CustomProviderSettings<providerName extends ProviderName> = {
|
||||
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
|
||||
}
|
||||
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
||||
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
|
||||
}
|
||||
|
||||
|
||||
type CommonProviderSettings = {
|
||||
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
|
||||
models: VoidModelInfo[],
|
||||
}
|
||||
|
||||
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
||||
// part of state
|
||||
export type SettingsOfProvider = {
|
||||
[providerName in ProviderName]: SettingsForProvider<providerName>
|
||||
}
|
||||
|
||||
|
||||
export type SettingName = keyof SettingsForProvider<ProviderName>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type DisplayInfoForProviderName = {
|
||||
title: string,
|
||||
}
|
||||
|
||||
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
|
||||
if (providerName === 'anthropic') {
|
||||
return {
|
||||
title: 'Anthropic',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openAI') {
|
||||
return {
|
||||
title: 'OpenAI',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
return {
|
||||
title: 'OpenRouter',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'ollama') {
|
||||
return {
|
||||
title: 'Ollama',
|
||||
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
return {
|
||||
title: 'OpenAI-Compatible',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
return {
|
||||
title: 'Gemini',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
return {
|
||||
title: 'Groq',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
||||
type DisplayInfo = {
|
||||
title: string,
|
||||
placeholder: string,
|
||||
subTextMd?: string,
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
if (settingName === 'apiKey') {
|
||||
return {
|
||||
title: 'API Key',
|
||||
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
|
||||
providerName === 'openAI' ? 'sk-proj-key...' :
|
||||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||
providerName === 'gemini' ? 'key...' :
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
'(never)',
|
||||
|
||||
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
|
||||
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
|
||||
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
|
||||
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
|
||||
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
|
||||
providerName === 'openAICompatible' ? undefined :
|
||||
undefined,
|
||||
}
|
||||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
|
||||
: '(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
|
||||
subTextMd: providerName === 'ollama' ? 'Read about Ollama [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
|
||||
undefined,
|
||||
}
|
||||
}
|
||||
else if (settingName === '_enabled') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
}
|
||||
}
|
||||
else if (settingName === 'models') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
}
|
||||
|
||||
export const voidInitModelOptions = {
|
||||
anthropic: {
|
||||
models: defaultAnthropicModels,
|
||||
},
|
||||
openAI: {
|
||||
models: defaultOpenAIModels,
|
||||
},
|
||||
ollama: {
|
||||
models: [],
|
||||
},
|
||||
openRouter: {
|
||||
models: [], // any string
|
||||
},
|
||||
openAICompatible: {
|
||||
models: [],
|
||||
},
|
||||
gemini: {
|
||||
models: defaultGeminiModels,
|
||||
},
|
||||
groq: {
|
||||
models: defaultGroqModels,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
},
|
||||
openAI: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
_enabled: undefined,
|
||||
},
|
||||
groq: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
_enabled: undefined,
|
||||
},
|
||||
ollama: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openRouter: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openAICompatible: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
_enabled: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export type ModelSelection = { providerName: ProviderName, modelName: string }
|
||||
|
||||
export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => {
|
||||
return m1.modelName === m2.modelName && m1.providerName === m2.providerName
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export type ModelSelectionOfFeature = {
|
||||
'Ctrl+L': ModelSelection | null,
|
||||
'Ctrl+K': ModelSelection | null,
|
||||
'Autocomplete': ModelSelection | null,
|
||||
}
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// the models of these can be refreshed (in theory all can, but not all should)
|
||||
export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[]
|
||||
export type RefreshableProviderName = typeof refreshableProviderNames[number]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export type FeatureFlagSettings = {
|
||||
autoRefreshModels: boolean;
|
||||
}
|
||||
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
|
||||
autoRefreshModels: true,
|
||||
}
|
||||
|
||||
export type FeatureFlagName = keyof FeatureFlagSettings
|
||||
export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[]
|
||||
|
||||
type FeatureFlagDisplayInfo = {
|
||||
description: string,
|
||||
}
|
||||
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
|
||||
if (featureFlag === 'autoRefreshModels') {
|
||||
return {
|
||||
description: `Automatically scan for and enable local models.`, // ${`refreshableProviderNames.map(providerName => titleOfProviderName(providerName)).join(', ')`}
|
||||
}
|
||||
}
|
||||
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// this channel is registered in `app.ts`
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but this is simpler.
|
||||
// you could create one instance in electron-main/my-service.ts and one in browser/my-service.ts (and define the interface IMyService in common/my-service.ts), but we just use a channel here
|
||||
// registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed);
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { sendLLMMessage } from '../../../workbench/contrib/void/browser/react/out/util/sendLLMMessage.js';
|
||||
import { listenerNames, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
|
||||
|
||||
// NODE IMPLEMENTATION OF SENDLLMMESSAGE - calls sendLLMMessage() and returns listeners
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
private readonly _onText = new Emitter<ProxyOnTextPayload>();
|
||||
readonly onText = this._onText.event;
|
||||
|
||||
private readonly _onFinalMessage = new Emitter<ProxyOnFinalMessagePayload>();
|
||||
readonly onFinalMessage = this._onFinalMessage.event;
|
||||
|
||||
private readonly _onError = new Emitter<ProxyOnErrorPayload>();
|
||||
readonly onError = this._onError.event;
|
||||
|
||||
|
||||
private readonly _abortRefOfRequestId: Record<string, AbortRef> = {}
|
||||
|
||||
|
||||
constructor() { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: typeof listenerNames[number]): Event<any> {
|
||||
if (event === 'onText') {
|
||||
return this.onText;
|
||||
}
|
||||
else if (event === 'onFinalMessage') {
|
||||
return this.onFinalMessage;
|
||||
}
|
||||
else if (event === 'onError') {
|
||||
return this.onError;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
}
|
||||
|
||||
// browser uses this to call
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
|
||||
try {
|
||||
if (command === 'sendLLMMessage') {
|
||||
this._callSendLLMMessage(params)
|
||||
}
|
||||
else if (command === 'abort') {
|
||||
this._callAbort(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log('llmMessageChannel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// the only place sendLLMMessage is actually called
|
||||
private _callSendLLMMessage(params: ProxyLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId))
|
||||
this._abortRefOfRequestId[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: SendLLMMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage.fire({ requestId, fullText }); },
|
||||
onError: ({ error }) => { this._onError.fire({ requestId, error }); },
|
||||
abortRef: this._abortRefOfRequestId[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams);
|
||||
}
|
||||
|
||||
private _callAbort(params: ProxyLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId)) return
|
||||
this._abortRefOfRequestId[requestId].current?.()
|
||||
delete this._abortRefOfRequestId[requestId]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
// Anthropic
|
||||
type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
|
||||
const maxTokens = anthropicMaxPossibleTokens(modelName)
|
||||
if (maxTokens === undefined) {
|
||||
onError({ message: `Please set a value for Max Tokens.`, fullError: null })
|
||||
return
|
||||
}
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// remove system messages for Anthropic
|
||||
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: systemMessage,
|
||||
messages: anthropicMessages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
});
|
||||
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText })
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
// stringify the response's content
|
||||
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
|
||||
onFinalMessage({ fullText: 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({ message: 'Invalid API key.', fullError: error })
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Gemini
|
||||
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const thisConfig = settingsOfProvider.gemini
|
||||
|
||||
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
|
||||
const model = genAI.getGenerativeModel({ model: modelName });
|
||||
|
||||
// remove system messages that get sent to Gemini
|
||||
// str of all system messages
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// Convert messages to Gemini format
|
||||
const geminiMessages: Content[] = messages
|
||||
.filter(msg => msg.role !== 'system')
|
||||
.map((msg, i) => ({
|
||||
parts: [{ text: msg.content }],
|
||||
role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
}))
|
||||
|
||||
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
|
||||
.then(async response => {
|
||||
_setAborter(() => response.stream.return(fullText))
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
const newText = chunk.text();
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
|
||||
onError({ message: 'Invalid API key.', fullError: null });
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// // Greptile
|
||||
// // https://docs.greptile.com/api-reference/query
|
||||
// // https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// const thisConfig = settingsOfProvider.greptile
|
||||
|
||||
// fetch('https://api.greptile.com/v2/query', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${thisConfig.apikey}`,
|
||||
// 'X-Github-Token': `${thisConfig.githubPAT}`,
|
||||
// 'Content-Type': `application/json`,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// messages,
|
||||
// stream: true,
|
||||
// repositories: [thisConfig.repoinfo],
|
||||
// }),
|
||||
// })
|
||||
// // this is {message}\n{message}\n{message}...\n
|
||||
// .then(async response => {
|
||||
// const text = await response.text()
|
||||
// console.log('got greptile', text)
|
||||
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
// })
|
||||
// // TODO make this actually stream, right now it just sends one message at the end
|
||||
// // TODO add _setAborter() when add streaming
|
||||
// .then(async responseArr => {
|
||||
|
||||
// for (const response of responseArr) {
|
||||
// const type: string = response['type']
|
||||
// const message = response['message']
|
||||
|
||||
// // when receive text
|
||||
// if (type === 'message') {
|
||||
// fullText += message
|
||||
// onText({ newText: message, fullText })
|
||||
// }
|
||||
// else if (type === 'sources') {
|
||||
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
|
||||
// fullText += filepath
|
||||
// onText({ newText: filepath, fullText })
|
||||
// }
|
||||
// // type: 'status' with an empty 'message' means last message
|
||||
// else if (type === 'status') {
|
||||
// if (!message) {
|
||||
// onFinalMessage({ fullText })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// })
|
||||
// .catch(error => {
|
||||
// onError({ error })
|
||||
// });
|
||||
|
||||
// }
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Groq from 'groq-sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Groq
|
||||
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
let fullText = '';
|
||||
|
||||
const thisConfig = settingsOfProvider.groq
|
||||
|
||||
const groq = new Groq({
|
||||
apiKey: thisConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
await groq.chat.completions
|
||||
.create({
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
temperature: 0.7,
|
||||
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
|
||||
})
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
if (newText) {
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
})
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
||||
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
ollama.list()
|
||||
.then((response) => {
|
||||
const { models } = response
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
// if (typeof error === 'object') {
|
||||
// const e = error.error as ErrorResponse['error']
|
||||
// if (e) {
|
||||
// const name = error.name ?? 'Error'
|
||||
// onError({ error: `${name}: ${e}` })
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', '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',]
|
||||
@@ -0,0 +1,105 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Model } from 'openai/resources/models.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
|
||||
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
const onSuccess = ({ models }: { models: Model[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: Model[] = []
|
||||
models.push(...response.data)
|
||||
while (response.hasNextPage()) {
|
||||
models.push(...(await response.getNextPage()).data)
|
||||
}
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider.openAI
|
||||
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider.openRouter
|
||||
openai = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.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: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
|
||||
throw new Error(`providerName was invalid: ${providerName}`)
|
||||
}
|
||||
|
||||
openai.models.list()
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error });
|
||||
}
|
||||
else {
|
||||
onError({ message: error, fullError: error });
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
|
||||
import { sendAnthropicMsg } from './anthropic.js';
|
||||
import { sendOllamaMsg } from './ollama.js';
|
||||
import { sendOpenAIMsg } from './openai.js';
|
||||
import { sendGeminiMsg } from './gemini.js';
|
||||
import { sendGroqMsg } from './groq.js';
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
messages,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
logging: { loggingName },
|
||||
settingsOfProvider,
|
||||
providerName,
|
||||
modelName,
|
||||
}: LLMMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
) => {
|
||||
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
version: '2024-11-14',
|
||||
...extras,
|
||||
})
|
||||
}
|
||||
const submit_time = new Date()
|
||||
|
||||
let _fullTextSoFar = ''
|
||||
let _aborter: (() => void) | null = null
|
||||
let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
let _didAbort = false
|
||||
|
||||
const onText: OnText = ({ newText, fullText }) => {
|
||||
if (_didAbort) return
|
||||
onText_({ newText, fullText })
|
||||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_({ fullText })
|
||||
}
|
||||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
if (_didAbort) return
|
||||
console.log("ERROR!!!!!", error)
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
try { _aborter?.() } // aborter sometimes automatically throws an error
|
||||
catch (e) { }
|
||||
_didAbort = true
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
|
||||
|
||||
try {
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'ollama':
|
||||
sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'groq':
|
||||
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
default:
|
||||
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
|
||||
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
|
||||
// ; (_aborter as any)?.()
|
||||
// _didAbort = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// registered in app.ts
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ollamaList } from './llmMessage/ollama.js';
|
||||
import { openaiCompatibleList } from './llmMessage/openai.js';
|
||||
|
||||
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
// sendLLMMessage
|
||||
private readonly _onText_llm = new Emitter<EventLLMMessageOnTextParams>();
|
||||
private readonly _onFinalMessage_llm = new Emitter<EventLLMMessageOnFinalMessageParams>();
|
||||
private readonly _onError_llm = new Emitter<EventLLMMessageOnErrorParams>();
|
||||
|
||||
// abort
|
||||
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
|
||||
|
||||
// ollamaList
|
||||
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
|
||||
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
|
||||
|
||||
// openaiCompatibleList
|
||||
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
|
||||
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
|
||||
|
||||
// stupidly, channels can't take in @IService
|
||||
constructor(
|
||||
private readonly metricsService: IMetricsService,
|
||||
) { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
if (event === 'onText_llm') {
|
||||
return this._onText_llm.event;
|
||||
}
|
||||
else if (event === 'onFinalMessage_llm') {
|
||||
return this._onFinalMessage_llm.event;
|
||||
}
|
||||
else if (event === 'onError_llm') {
|
||||
return this._onError_llm.event;
|
||||
}
|
||||
else if (event === 'onSuccess_ollama') {
|
||||
return this._onSuccess_ollama.event;
|
||||
}
|
||||
else if (event === 'onError_ollama') {
|
||||
return this._onError_ollama.event;
|
||||
}
|
||||
else if (event === 'onSuccess_openAICompatible') {
|
||||
return this._onSuccess_openAICompatible.event;
|
||||
}
|
||||
else if (event === 'onError_openAICompatible') {
|
||||
return this._onError_openAICompatible.event;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
}
|
||||
|
||||
// browser uses this to call
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
try {
|
||||
if (command === 'sendLLMMessage') {
|
||||
this._callSendLLMMessage(params)
|
||||
}
|
||||
else if (command === 'abort') {
|
||||
this._callAbort(params)
|
||||
}
|
||||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else if (command === 'openAICompatibleList') {
|
||||
this._callOpenAICompatibleList(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log('llmMessageChannel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// the only place sendLLMMessage is actually called
|
||||
private async _callSendLLMMessage(params: MainLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId_llm))
|
||||
this._abortRefOfRequestId_llm[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: LLMMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
|
||||
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
|
||||
abortRef: this._abortRefOfRequestId_llm[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams, this.metricsService);
|
||||
}
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId_llm)) return
|
||||
this._abortRefOfRequestId_llm[requestId].current?.()
|
||||
delete this._abortRefOfRequestId_llm[requestId]
|
||||
}
|
||||
|
||||
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
|
||||
}
|
||||
ollamaList(mainThreadParams)
|
||||
}
|
||||
|
||||
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
|
||||
}
|
||||
openaiCompatibleList(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
|
||||
// posthog-js (old):
|
||||
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
|
||||
|
||||
// const buildEnv = 'development';
|
||||
// const buildNumber = '1.0.0';
|
||||
// const isMac = process.platform === 'darwin';
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
readonly _distinctId: string
|
||||
readonly client: PostHog
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
super()
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
|
||||
|
||||
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
|
||||
this._distinctId = devDeviceId
|
||||
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
|
||||
}
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this._distinctId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -742,18 +742,6 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
|
||||
cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) });
|
||||
});
|
||||
|
||||
|
||||
// // Void: send from https://
|
||||
// this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => {
|
||||
// // const voidConfig = this.voidConfigStateService.state.voidConfig
|
||||
// // const whichApi = voidConfig.default['whichApi']
|
||||
// const endpoint = 'http://127.' //string | undefined = voidConfig[whichApi as VoidConfigField].endpoint
|
||||
|
||||
// if (endpoint && details.url.startsWith(endpoint)) {
|
||||
// details.requestHeaders['Origin'] = 'https://app.voideditor.com'
|
||||
// }
|
||||
// cb({ cancel: false, requestHeaders: details.requestHeaders });
|
||||
// });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ MenuRegistry.appendMenuItems([{
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleSidebarPositionAction.ID,
|
||||
title: localize('move second sidebar left', "Move Secondary Side Bar Left")
|
||||
title: localize('move second sidebar left', "Move Void Side Bar Left")
|
||||
},
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 1
|
||||
@@ -250,7 +250,7 @@ MenuRegistry.appendMenuItems([{
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleSidebarPositionAction.ID,
|
||||
title: localize('move second sidebar right', "Move Secondary Side Bar Right")
|
||||
title: localize('move second sidebar right', "Move Void Side Bar Right")
|
||||
},
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 1
|
||||
@@ -949,7 +949,7 @@ registerAction2(class extends Action2 {
|
||||
if (!hasAddedView) {
|
||||
results.push({
|
||||
type: 'separator',
|
||||
label: localize('secondarySideBarContainer', "Secondary Side Bar / {0}", containerModel.title)
|
||||
label: localize('secondarySideBarContainer', "Void Side Bar / {0}", containerModel.title)
|
||||
});
|
||||
hasAddedView = true;
|
||||
}
|
||||
@@ -1056,7 +1056,7 @@ class MoveFocusedViewAction extends Action2 {
|
||||
if (!(isViewSolo && currentLocation === ViewContainerLocation.AuxiliaryBar)) {
|
||||
items.push({
|
||||
id: '_.auxiliarybar.newcontainer',
|
||||
label: localize('moveFocusedView.newContainerInSidePanel', "New Secondary Side Bar Entry")
|
||||
label: localize('moveFocusedView.newContainerInSidePanel', "New Void Side Bar Entry")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1104,7 +1104,7 @@ class MoveFocusedViewAction extends Action2 {
|
||||
|
||||
items.push({
|
||||
type: 'separator',
|
||||
label: localize('secondarySideBar', "Secondary Side Bar")
|
||||
label: localize('secondarySideBar', "Void Side Bar")
|
||||
});
|
||||
|
||||
const pinnedAuxPanels = paneCompositePartService.getPinnedPaneCompositeIds(ViewContainerLocation.AuxiliaryBar);
|
||||
@@ -1386,7 +1386,7 @@ if (!isMacintosh || !isNative) {
|
||||
ToggleVisibilityActions.push(...[
|
||||
CreateToggleLayoutItem(ToggleActivityBarVisibilityActionId, ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'hidden'), localize('activityBar', "Activity Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: activityBarLeftIcon, iconB: activityBarRightIcon }),
|
||||
CreateToggleLayoutItem(ToggleSidebarVisibilityAction.ID, SideBarVisibleContext, localize('sideBar', "Primary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelLeftIcon, iconB: panelRightIcon }),
|
||||
CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Secondary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }),
|
||||
CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Void Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }),
|
||||
CreateToggleLayoutItem(TogglePanelAction.ID, PanelVisibleContext, localize('panel', "Panel"), panelIcon),
|
||||
CreateToggleLayoutItem(ToggleStatusbarVisibilityAction.ID, ContextKeyExpr.equals('config.workbench.statusBar.visible', true), localize('statusBar', "Status Bar"), statusBarIcon),
|
||||
]);
|
||||
|
||||
@@ -2434,7 +2434,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
|
||||
comment: 'Information about the layout of the workbench during statup';
|
||||
activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the activity bar is visible' };
|
||||
sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the primary side bar is visible' };
|
||||
auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the secondary side bar is visible' };
|
||||
auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the Void side bar is visible' };
|
||||
panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the panel is visible' };
|
||||
statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the status bar is visible' };
|
||||
sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' };
|
||||
|
||||
@@ -26,7 +26,7 @@ const auxiliaryBarLeftOffIcon = registerIcon('auxiliarybar-left-off-layout-icon'
|
||||
export class ToggleAuxiliaryBarAction extends Action2 {
|
||||
|
||||
static readonly ID = 'workbench.action.toggleAuxiliaryBar';
|
||||
static readonly LABEL = localize2('toggleAuxiliaryBar', "Toggle Secondary Side Bar Visibility");
|
||||
static readonly LABEL = localize2('toggleAuxiliaryBar', "Toggle Void Side Bar Visibility");
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -34,7 +34,7 @@ export class ToggleAuxiliaryBarAction extends Action2 {
|
||||
title: ToggleAuxiliaryBarAction.LABEL,
|
||||
toggled: {
|
||||
condition: AuxiliaryBarVisibleContext,
|
||||
title: localize('secondary sidebar', "Secondary Side Bar"),
|
||||
title: localize('secondary sidebar', "Void Side Bar"),
|
||||
mnemonicTitle: localize({ key: 'secondary sidebar mnemonic', comment: ['&& denotes a mnemonic'] }, "Secondary Si&&de Bar"),
|
||||
},
|
||||
|
||||
@@ -70,7 +70,7 @@ registerAction2(ToggleAuxiliaryBarAction);
|
||||
registerAction2(class FocusAuxiliaryBarAction extends Action2 {
|
||||
|
||||
static readonly ID = 'workbench.action.focusAuxiliaryBar';
|
||||
static readonly LABEL = localize2('focusAuxiliaryBar', "Focus into Secondary Side Bar");
|
||||
static readonly LABEL = localize2('focusAuxiliaryBar', "Focus into Void Side Bar");
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -103,7 +103,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '0_workbench_toggles',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"),
|
||||
title: localize('toggleSecondarySideBar', "Toggle Void Side Bar"),
|
||||
toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarLeftIcon },
|
||||
icon: auxiliaryBarLeftOffIcon,
|
||||
},
|
||||
@@ -116,7 +116,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '0_workbench_toggles',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"),
|
||||
title: localize('toggleSecondarySideBar', "Toggle Void Side Bar"),
|
||||
toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarRightIcon },
|
||||
icon: auxiliaryBarRightOffIcon,
|
||||
},
|
||||
@@ -129,7 +129,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize2('hideAuxiliaryBar', 'Hide Secondary Side Bar'),
|
||||
title: localize2('hideAuxiliaryBar', 'Hide Void Side Bar'),
|
||||
},
|
||||
when: ContextKeyExpr.and(AuxiliaryBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 2
|
||||
|
||||
@@ -195,11 +195,13 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
|
||||
const positionActions: IAction[] = [];
|
||||
createAndFillInContextMenuActions(activityBarPositionMenu, { primary: [], secondary: positionActions });
|
||||
|
||||
|
||||
// appears when right click
|
||||
actions.push(...[
|
||||
new Separator(),
|
||||
new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions),
|
||||
toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Secondary Side Bar Left") : localize('move second side bar right', "Move Secondary Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }),
|
||||
toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Secondary Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) })
|
||||
toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Void Side Bar Left") : localize('move second side bar right', "Move Void Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }),
|
||||
toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Void Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) })
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,82 +4,113 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { isMacintosh, isWeb, OS } from '../../../../base/common/platform.js';
|
||||
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { append, clearNode, $, h } from '../../../../base/browser/dom.js';
|
||||
import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
|
||||
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
|
||||
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { editorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { ColorScheme } from '../../../../platform/theme/common/theme.js';
|
||||
import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js';
|
||||
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
|
||||
import { isMacintosh, isNative, OS } from '../../../../base/common/platform.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from '../../../contrib/void/browser/quickEditActions.js';
|
||||
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
|
||||
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
|
||||
import { splitRecentLabel } from '../../../../base/common/labels.js';
|
||||
import { IHostService } from '../../../services/host/browser/host.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
|
||||
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
|
||||
registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.'));
|
||||
|
||||
interface WatermarkEntry {
|
||||
readonly text: string;
|
||||
readonly id: string;
|
||||
readonly mac?: boolean;
|
||||
readonly when?: ContextKeyExpression;
|
||||
}
|
||||
// interface WatermarkEntry {
|
||||
// readonly text: string;
|
||||
// readonly id: string;
|
||||
// readonly mac?: boolean;
|
||||
// readonly when?: ContextKeyExpression;
|
||||
// }
|
||||
|
||||
const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
|
||||
const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
|
||||
const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
|
||||
const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
|
||||
const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
|
||||
const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
|
||||
const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
|
||||
const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
|
||||
const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
|
||||
const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
|
||||
// const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
|
||||
// const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
|
||||
// const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
|
||||
// const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
|
||||
// const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
|
||||
// const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
|
||||
// const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
|
||||
// const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
|
||||
// const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
// const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
// const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
|
||||
// const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
|
||||
|
||||
const noFolderEntries = [
|
||||
showCommands,
|
||||
openFileNonMacOnly,
|
||||
openFolderNonMacOnly,
|
||||
openFileOrFolderMacOnly,
|
||||
openRecent,
|
||||
newUntitledFileMacOnly
|
||||
];
|
||||
// // shown when Void is emtpty
|
||||
// const noFolderEntries = [
|
||||
// // showCommands,
|
||||
// openFileNonMacOnly,
|
||||
// openFolderNonMacOnly,
|
||||
// openFileOrFolderMacOnly,
|
||||
// openRecent,
|
||||
// // newUntitledFileMacOnly
|
||||
// ];
|
||||
|
||||
const folderEntries = [
|
||||
showCommands,
|
||||
quickAccess,
|
||||
findInFiles,
|
||||
startDebugging,
|
||||
toggleTerminal,
|
||||
toggleFullscreen,
|
||||
showSettings
|
||||
];
|
||||
// const folderEntries = [
|
||||
// showCommands,
|
||||
// // quickAccess,
|
||||
// // findInFiles,
|
||||
// // startDebugging,
|
||||
// // toggleTerminal,
|
||||
// // toggleFullscreen,
|
||||
// // showSettings
|
||||
// ];
|
||||
|
||||
export class EditorGroupWatermark extends Disposable {
|
||||
private readonly shortcuts: HTMLElement;
|
||||
private readonly transientDisposables = this._register(new DisposableStore());
|
||||
private enabled: boolean = false;
|
||||
// private enabled: boolean = false;
|
||||
private workbenchState: WorkbenchState;
|
||||
private keybindingLabels = new Set<KeybindingLabel>();
|
||||
private currentDisposables = new Set<IDisposable>();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
// @IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const elements = h('.editor-group-watermark', [
|
||||
h('.letterpress'),
|
||||
h('.letterpress@icon'),
|
||||
h('.shortcuts@shortcuts'),
|
||||
]);
|
||||
|
||||
append(container, elements.root);
|
||||
this.shortcuts = elements.shortcuts;
|
||||
this.shortcuts = elements.shortcuts; // shortcuts div is modified on render()
|
||||
|
||||
// void icon style
|
||||
const updateTheme = () => {
|
||||
const theme = this.themeService.getColorTheme().type
|
||||
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
|
||||
elements.icon.style.maxWidth = '220px'
|
||||
elements.icon.style.opacity = '50%'
|
||||
elements.icon.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
|
||||
}
|
||||
updateTheme()
|
||||
this._register(
|
||||
this.themeService.onDidColorThemeChange(updateTheme)
|
||||
)
|
||||
|
||||
this.registerListeners();
|
||||
|
||||
@@ -103,56 +134,162 @@ export class EditorGroupWatermark extends Disposable {
|
||||
this.render();
|
||||
}));
|
||||
|
||||
const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
|
||||
const allKeys = new Set<string>();
|
||||
allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
|
||||
this._register(this.contextKeyService.onDidChangeContext(e => {
|
||||
if (e.affectsSome(allKeys)) {
|
||||
this.render();
|
||||
}
|
||||
}));
|
||||
// const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
|
||||
// const allKeys = new Set<string>();
|
||||
// allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
|
||||
// this._register(this.contextKeyService.onDidChangeContext(e => {
|
||||
// if (e.affectsSome(allKeys)) {
|
||||
// this.render();
|
||||
// }
|
||||
// }));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private render(): void {
|
||||
const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
|
||||
// const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
|
||||
|
||||
if (enabled === this.enabled) {
|
||||
return;
|
||||
}
|
||||
// if (enabled === this.enabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.enabled = enabled;
|
||||
|
||||
|
||||
// if (!enabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const hasFolder = this.workbenchState !== WorkbenchState.EMPTY;
|
||||
// const selected = (hasFolder ? folderEntries : noFolderEntries)
|
||||
// .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
|
||||
// .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
|
||||
// .filter(entry => !!CommandsRegistry.getCommand(entry.id))
|
||||
// .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
|
||||
|
||||
this.enabled = enabled;
|
||||
this.clear();
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const box = append(this.shortcuts, $('.watermark-box'));
|
||||
const folder = this.workbenchState !== WorkbenchState.EMPTY;
|
||||
const selected = (folder ? folderEntries : noFolderEntries)
|
||||
.filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
|
||||
.filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
|
||||
.filter(entry => !!CommandsRegistry.getCommand(entry.id))
|
||||
.filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
|
||||
const boxBelow = append(this.shortcuts, $(''))
|
||||
|
||||
|
||||
const update = async () => {
|
||||
|
||||
const update = () => {
|
||||
clearNode(box);
|
||||
this.keybindingLabels.forEach(label => label.dispose());
|
||||
this.keybindingLabels.clear();
|
||||
clearNode(boxBelow);
|
||||
|
||||
for (const entry of selected) {
|
||||
const keys = this.keybindingService.lookupKeybinding(entry.id);
|
||||
if (!keys) {
|
||||
continue;
|
||||
this.currentDisposables.forEach(label => label.dispose());
|
||||
this.currentDisposables.clear();
|
||||
|
||||
|
||||
// Void - if the workbench is empty, show open
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
|
||||
// Open Folder
|
||||
const button = h('button')
|
||||
button.root.textContent = 'Open Folder'
|
||||
button.root.onclick = () => {
|
||||
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
|
||||
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
|
||||
// this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID);
|
||||
// } else {
|
||||
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
|
||||
// }
|
||||
}
|
||||
box.appendChild(button.root);
|
||||
|
||||
// Recents
|
||||
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
|
||||
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
|
||||
|
||||
|
||||
|
||||
box.append(
|
||||
...recentlyOpened.map(w => {
|
||||
|
||||
let fullPath: string;
|
||||
let windowOpenable: IWindowOpenable;
|
||||
if (isRecentFolder(w)) {
|
||||
windowOpenable = { folderUri: w.folderUri };
|
||||
fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG });
|
||||
}
|
||||
else {
|
||||
return null
|
||||
// fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG });
|
||||
// windowOpenable = { workspaceUri: w.workspace.configPath };
|
||||
}
|
||||
|
||||
|
||||
|
||||
const { name, parentPath } = splitRecentLabel(fullPath);
|
||||
|
||||
const li = $('li');
|
||||
const link = $('button.button-link');
|
||||
|
||||
link.innerText = name;
|
||||
link.title = fullPath;
|
||||
link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath));
|
||||
link.addEventListener('click', e => {
|
||||
this.hostService.openWindow([windowOpenable], {
|
||||
forceNewWindow: e.ctrlKey || e.metaKey,
|
||||
remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
li.appendChild(link);
|
||||
|
||||
const span = $('span');
|
||||
span.classList.add('path');
|
||||
span.classList.add('detail');
|
||||
span.innerText = parentPath;
|
||||
span.title = fullPath;
|
||||
li.appendChild(span);
|
||||
|
||||
|
||||
return li
|
||||
}).filter(v => !!v)
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
// show them Void keybindings
|
||||
const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID);
|
||||
const dl = append(box, $('dl'));
|
||||
const dt = append(dl, $('dt'));
|
||||
dt.textContent = entry.text;
|
||||
dt.textContent = 'Chat'
|
||||
const dd = append(dl, $('dd'));
|
||||
const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
label.set(keys);
|
||||
this.keybindingLabels.add(label);
|
||||
if (keys)
|
||||
label.set(keys);
|
||||
this.currentDisposables.add(label);
|
||||
|
||||
|
||||
const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID);
|
||||
const dl2 = append(box, $('dl'));
|
||||
const dt2 = append(dl2, $('dt'));
|
||||
dt2.textContent = 'Quick Edit'
|
||||
const dd2 = append(dl2, $('dd'));
|
||||
const label2 = new KeybindingLabel(dd2, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
if (keys2)
|
||||
label2.set(keys2);
|
||||
this.currentDisposables.add(label2);
|
||||
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(boxBelow, $('button'));
|
||||
button3.textContent = 'Void Settings'
|
||||
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
if (keys3)
|
||||
label3.set(keys3);
|
||||
button3.onclick = () => {
|
||||
this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
}
|
||||
this.currentDisposables.add(label3);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
update();
|
||||
@@ -167,6 +304,6 @@ export class EditorGroupWatermark extends Disposable {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.clear();
|
||||
this.keybindingLabels.forEach(label => label.dispose());
|
||||
this.currentDisposables.forEach(label => label.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty {
|
||||
opacity: 0.5; /* dimmed to indicate inactive state */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty {
|
||||
opacity: 0.5;
|
||||
/* dimmed to indicate inactive state */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.active,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.dragged-over {
|
||||
opacity: 1; /* indicate active/dragged-over group through undimmed state */
|
||||
opacity: 1;
|
||||
/* indicate active/dragged-over group through undimmed state */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty.active:focus {
|
||||
@@ -24,12 +26,13 @@
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content.empty .editor-group-container.empty.active:focus {
|
||||
outline: none; /* never show outline for empty group if it is the last */
|
||||
outline: none;
|
||||
/* never show outline for empty group if it is the last */
|
||||
}
|
||||
|
||||
/* Watermark & shortcuts */
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-width: 290px;
|
||||
@@ -49,26 +52,27 @@
|
||||
height: calc(100% - 70px);
|
||||
}
|
||||
|
||||
/* light */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
background-image: url('./letterpress-light.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
background-size: contain;
|
||||
background-position-x: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-dark.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-hcLight.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-hcDark.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark > .shortcuts,
|
||||
@@ -109,12 +113,13 @@
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title:not(.tabs) {
|
||||
display: flex; /* when tabs are not shown, use flex layout */
|
||||
display: flex;
|
||||
/* when tabs are not shown, use flex layout */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
@@ -144,7 +149,8 @@
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.locked > .editor-group-container-toolbar,
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-container-toolbar,
|
||||
.monaco-workbench .part.editor > .content.auxiliary .editor-group-container.empty > .editor-group-container-toolbar {
|
||||
display: block; /* show toolbar when more than one editor group or always when auxiliary or locked */
|
||||
display: block;
|
||||
/* show toolbar when more than one editor group or always when auxiliary or locked */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .actions-container {
|
||||
@@ -157,7 +163,7 @@
|
||||
|
||||
/* Editor */
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 850 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
@@ -342,7 +342,7 @@ registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.closeAuxiliaryBar',
|
||||
title: localize2('closeSecondarySideBar', 'Hide Secondary Side Bar'),
|
||||
title: localize2('closeSecondarySideBar', 'Hide Void Side Bar'),
|
||||
category: Categories.View,
|
||||
icon: closeIcon,
|
||||
menu: [{
|
||||
@@ -415,14 +415,16 @@ class MoveViewsBetweenPanelsAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Move Panel Views To Secondary Side Bar
|
||||
// --- Move Panel Views To Void Side Bar
|
||||
|
||||
// these are just for the command pallette
|
||||
|
||||
class MovePanelToSidePanelAction extends MoveViewsBetweenPanelsAction {
|
||||
static readonly ID = 'workbench.action.movePanelToSidePanel';
|
||||
constructor() {
|
||||
super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, {
|
||||
id: MovePanelToSidePanelAction.ID,
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"),
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Void Side Bar"),
|
||||
category: Categories.View,
|
||||
f1: false
|
||||
});
|
||||
@@ -434,7 +436,7 @@ export class MovePanelToSecondarySideBarAction extends MoveViewsBetweenPanelsAct
|
||||
constructor() {
|
||||
super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, {
|
||||
id: MovePanelToSecondarySideBarAction.ID,
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"),
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Void Side Bar"),
|
||||
category: Categories.View,
|
||||
f1: true
|
||||
});
|
||||
@@ -452,7 +454,7 @@ class MoveSidePanelToPanelAction extends MoveViewsBetweenPanelsAction {
|
||||
constructor() {
|
||||
super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, {
|
||||
id: MoveSidePanelToPanelAction.ID,
|
||||
title: localize2('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"),
|
||||
title: localize2('moveSidePanelToPanel', "Move Side Bar Views To Panel"), // Void - this seemed to have a typo before
|
||||
category: Categories.View,
|
||||
f1: false
|
||||
});
|
||||
@@ -465,7 +467,7 @@ export class MoveSecondarySideBarToPanelAction extends MoveViewsBetweenPanelsAct
|
||||
constructor() {
|
||||
super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, {
|
||||
id: MoveSecondarySideBarToPanelAction.ID,
|
||||
title: localize2('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"),
|
||||
title: localize2('moveSidePanelToPanel', "Move Void Side Bar Views To Panel"),
|
||||
category: Categories.View,
|
||||
f1: true
|
||||
});
|
||||
|
||||
@@ -512,7 +512,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'type': 'string',
|
||||
'enum': ['left', 'right'],
|
||||
'default': 'left',
|
||||
'description': localize('sideBarLocation', "Controls the location of the primary side bar and activity bar. They can either show on the left or right of the workbench. The secondary side bar will show on the opposite side of the workbench.")
|
||||
'description': localize('sideBarLocation', "Controls the location of the primary side bar and activity bar. They can either show on the left or right of the workbench. The Void side bar will show on the opposite side of the workbench.")
|
||||
},
|
||||
'workbench.panel.showLabel': {
|
||||
'type': 'boolean',
|
||||
@@ -545,12 +545,12 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'type': 'string',
|
||||
'enum': ['default', 'top', 'bottom', 'hidden'],
|
||||
'default': 'default',
|
||||
'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Secondary Side Bars."),
|
||||
'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Void Side Bars."),
|
||||
'enumDescriptions': [
|
||||
localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Secondary Side Bar."),
|
||||
localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bars."),
|
||||
localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bars."),
|
||||
localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bars.")
|
||||
localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Void Side Bar."),
|
||||
localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Void Side Bars."),
|
||||
localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Void Side Bars."),
|
||||
localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Void Side Bars.")
|
||||
],
|
||||
},
|
||||
'workbench.activityBar.iconClickBehavior': {
|
||||
@@ -598,7 +598,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
|
||||
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
|
||||
// On Mac, the delay is 1500.
|
||||
'default': isMacintosh ? 1500 : 500,
|
||||
'default': isMacintosh ? 300 : 300, // <-- Void edited this to 300 : 300 (was 1500 : 500)
|
||||
'minimum': 0
|
||||
},
|
||||
'workbench.reduceMotion': {
|
||||
|
||||
@@ -151,7 +151,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider<IViewQuic
|
||||
// Viewlets / Panels
|
||||
addPaneComposites(ViewContainerLocation.Sidebar, localize('views', "Side Bar"));
|
||||
addPaneComposites(ViewContainerLocation.Panel, localize('panels', "Panel"));
|
||||
addPaneComposites(ViewContainerLocation.AuxiliaryBar, localize('secondary side bar', "Secondary Side Bar"));
|
||||
addPaneComposites(ViewContainerLocation.AuxiliaryBar, localize('secondary side bar', "Void Side Bar"));
|
||||
|
||||
const addPaneCompositeViews = (location: ViewContainerLocation) => {
|
||||
const paneComposites = this.paneCompositeService.getPaneComposites(location);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
void-imports/
|
||||
@@ -0,0 +1,770 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { InlineCompletion, InlineCompletionContext } from '../../../../editor/common/languages.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { EditorResourceAccessor } from '../../../common/editor.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
|
||||
/*
|
||||
A summary of autotab:
|
||||
|
||||
Postprocessing
|
||||
-one common problem for all models is outputting unbalanced parentheses
|
||||
we solve this by trimming all extra closing parentheses from the generated string
|
||||
in future, should make sure parentheses are always balanced
|
||||
|
||||
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
|
||||
we complete up to first matchup character
|
||||
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
|
||||
|
||||
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
|
||||
this should happen automatically with caching system
|
||||
should break preloaded responses into \n\n chunks
|
||||
|
||||
Preprocessing
|
||||
- we don't generate if cursor is at end / beginning of a line (no spaces)
|
||||
- we generate 1 line if there is text to the right of cursor
|
||||
- we generate 1 line if variable declaration
|
||||
- (in many cases want to show 1 line but generate multiple)
|
||||
|
||||
State
|
||||
- cache based on prefix (and do some trimming first)
|
||||
- when press tab on one line, should have an immediate followup response
|
||||
to do this, show autocompletes before they're fully finished
|
||||
- [todo] remove each autotab when accepted
|
||||
!- [todo] provide type information
|
||||
|
||||
Details
|
||||
-generated results are trimmed up to 1 leading/trailing space
|
||||
-prefixes are cached up to 1 trailing newline
|
||||
-
|
||||
*/
|
||||
|
||||
class LRUCache<K, V> {
|
||||
public items: Map<K, V>;
|
||||
private keyOrder: K[];
|
||||
private maxSize: number;
|
||||
private disposeCallback?: (value: V, key?: K) => void;
|
||||
|
||||
constructor(maxSize: number, disposeCallback?: (value: V, key?: K) => void) {
|
||||
if (maxSize <= 0) throw new Error('Cache size must be greater than 0');
|
||||
|
||||
this.items = new Map();
|
||||
this.keyOrder = [];
|
||||
this.maxSize = maxSize;
|
||||
this.disposeCallback = disposeCallback;
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
// If key exists, remove it from the order list
|
||||
if (this.items.has(key)) {
|
||||
this.keyOrder = this.keyOrder.filter(k => k !== key);
|
||||
}
|
||||
// If cache is full, remove least recently used item
|
||||
else if (this.items.size >= this.maxSize) {
|
||||
const key = this.keyOrder[0];
|
||||
const value = this.items.get(key);
|
||||
|
||||
// Call dispose callback if it exists
|
||||
if (this.disposeCallback && value !== undefined) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
|
||||
this.items.delete(key);
|
||||
this.keyOrder.shift();
|
||||
}
|
||||
|
||||
// Add new item
|
||||
this.items.set(key, value);
|
||||
this.keyOrder.push(key);
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const value = this.items.get(key);
|
||||
|
||||
if (value !== undefined) {
|
||||
// Call dispose callback if it exists
|
||||
if (this.disposeCallback) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
|
||||
this.items.delete(key);
|
||||
this.keyOrder = this.keyOrder.filter(k => k !== key);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
// Call dispose callback for all items if it exists
|
||||
if (this.disposeCallback) {
|
||||
for (const [key, value] of this.items.entries()) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
}
|
||||
|
||||
this.items.clear();
|
||||
this.keyOrder = [];
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.items.size;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.items.has(key);
|
||||
}
|
||||
}
|
||||
|
||||
type AutocompletionStatus = 'pending' | 'finished' | 'error';
|
||||
type Autocompletion = {
|
||||
id: number,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
startTime: number,
|
||||
endTime: number | undefined,
|
||||
status: AutocompletionStatus,
|
||||
llmPromise: Promise<string> | undefined,
|
||||
insertText: string,
|
||||
requestId: string | null,
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIME = 500
|
||||
const TIMEOUT_TIME = 60000
|
||||
const MAX_CACHE_SIZE = 20
|
||||
const MAX_PENDING_REQUESTS = 2
|
||||
|
||||
// postprocesses the result
|
||||
const postprocessResult = (result: string) => {
|
||||
|
||||
// trim all whitespace except for a single leading/trailing space
|
||||
// return result.trim()
|
||||
|
||||
const hasLeadingSpace = result.startsWith(' ');
|
||||
const hasTrailingSpace = result.endsWith(' ');
|
||||
return (hasLeadingSpace ? ' ' : '')
|
||||
+ result.trim()
|
||||
+ (hasTrailingSpace ? ' ' : '');
|
||||
|
||||
}
|
||||
|
||||
const extractCodeFromResult = (result: string) => {
|
||||
// Match either:
|
||||
// 1. ```language\n<code>```
|
||||
// 2. ```<code>```
|
||||
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
|
||||
|
||||
if (!match) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return whichever group matched (non-empty)
|
||||
return match[1] ?? match[2] ?? result;
|
||||
}
|
||||
|
||||
|
||||
// trims the end of the prefix to improve cache hit rate
|
||||
const removeLeftTabsAndTrimEnd = (s: string): string => {
|
||||
const trimmedString = s.trimEnd();
|
||||
const trailingEnd = s.slice(trimmedString.length);
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes('\n')) {
|
||||
s = trimmedString + '\n';
|
||||
}
|
||||
|
||||
s = s.replace(/^\s+/gm, ''); // remove left tabs
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
|
||||
|
||||
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
|
||||
|
||||
// process all bracets in prefix
|
||||
let stack: string[] = []
|
||||
const firstOpenIdx = prefix.search(/[[({]/);
|
||||
if (firstOpenIdx !== -1) {
|
||||
const brackets = prefix.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c));
|
||||
|
||||
for (const bracket of brackets) {
|
||||
if (bracket === '(' || bracket === '{' || bracket === '[') {
|
||||
stack.push(bracket);
|
||||
} else {
|
||||
if (stack.length > 0 && stack[stack.length - 1] === pairs[bracket]) {
|
||||
stack.pop();
|
||||
} else {
|
||||
stack.push(bracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through each character
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const char = s[i];
|
||||
|
||||
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
|
||||
else if (char === ')' || char === '}' || char === ']') {
|
||||
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
const parenthesisChars = `{}()[]<>\`'"`
|
||||
|
||||
// returns the text in the autocompletion to display, assuming the prefix is already matched
|
||||
const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, position, debug }: { matchInfo: matchInfo, prefix: string, suffix: string, autocompletion: Autocompletion, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
|
||||
|
||||
|
||||
const suffixLines = suffix.split('\n')
|
||||
const prefixLines = prefix.split('\n')
|
||||
const suffixToTheRightOfCursor = suffixLines[0]
|
||||
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1]
|
||||
const generatedMiddle = autocompletion.insertText
|
||||
|
||||
let startIdx = matchInfo.startIdx
|
||||
let endIdx = generatedMiddle.length // exclusive bounds
|
||||
|
||||
// const naiveReturnValue = generatedMiddle.slice(startIdx)
|
||||
// console.log('naiveReturnValue: ', JSON.stringify(naiveReturnValue))
|
||||
// return [{ insertText: naiveReturnValue, }]
|
||||
|
||||
// do postprocessing for better ux
|
||||
// this is a bit hacky but may change a lot
|
||||
|
||||
// if there is space at the start of the completion and user has added it, remove it
|
||||
const charToLeftOfCursor = prefixToTheLeftOfCursor.slice(-1)[0] || ''
|
||||
const userHasAddedASpace = charToLeftOfCursor === ' ' || charToLeftOfCursor === '\t'
|
||||
const rawFirstNonspaceIdx = generatedMiddle.slice(startIdx).search(/[^\t ]/)
|
||||
if (rawFirstNonspaceIdx > -1 && userHasAddedASpace) {
|
||||
const firstNonspaceIdx = rawFirstNonspaceIdx + startIdx;
|
||||
// console.log('p0', startIdx, rawFirstNonspaceIdx)
|
||||
startIdx = Math.max(startIdx, firstNonspaceIdx)
|
||||
}
|
||||
|
||||
// if user is on a blank line and the generation starts with newline(s), remove them
|
||||
const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0;
|
||||
if (
|
||||
!prefixToTheLeftOfCursor.trim()
|
||||
&& !suffixToTheRightOfCursor.trim()
|
||||
&& numStartingNewlines > 0
|
||||
) {
|
||||
// console.log('p1', numStartingNewlines)
|
||||
startIdx += numStartingNewlines
|
||||
}
|
||||
|
||||
// if the generated text matches with the suffix on the current line, stop
|
||||
if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
|
||||
// complete until there is a match
|
||||
const rawMatchIndex = generatedMiddle.slice(startIdx).lastIndexOf(suffixToTheRightOfCursor.trim()[0])
|
||||
if (rawMatchIndex > -1) {
|
||||
// console.log('p2', rawMatchIndex, startIdx, suffixToTheRightOfCursor.trim()[0], 'AAA', generatedMiddle.slice(startIdx))
|
||||
const matchIdx = rawMatchIndex + startIdx;
|
||||
const matchChar = generatedMiddle[matchIdx]
|
||||
if (parenthesisChars.includes(matchChar)) {
|
||||
endIdx = Math.min(endIdx, matchIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? ''
|
||||
// condition to complete as a single line completion
|
||||
if (
|
||||
prefixToTheLeftOfCursor.trim()
|
||||
&& !suffixToTheRightOfCursor.trim()
|
||||
&& restOfLineToGenerate.trim()
|
||||
) {
|
||||
|
||||
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n')
|
||||
if (rawNewlineIdx > -1) {
|
||||
// console.log('p3', startIdx, rawNewlineIdx)
|
||||
const newlineIdx = rawNewlineIdx + startIdx;
|
||||
endIdx = Math.min(endIdx, newlineIdx)
|
||||
}
|
||||
}
|
||||
|
||||
// // if a generated line matches with a suffix line, stop
|
||||
// if (suffixLines.length > 1) {
|
||||
// console.log('4')
|
||||
// const lines = []
|
||||
// for (const generatedLine of generatedLines) {
|
||||
// if (suffixLines.slice(0, 10).some(suffixLine =>
|
||||
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
|
||||
// && generatedLine.trim().startsWith(suffixLine.trim())
|
||||
// )) break;
|
||||
// lines.push(generatedLine)
|
||||
// }
|
||||
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
|
||||
// }
|
||||
|
||||
// console.log('pFinal', startIdx, endIdx)
|
||||
let completionStr = generatedMiddle.slice(startIdx, endIdx)
|
||||
|
||||
// filter out unbalanced parentheses
|
||||
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix)
|
||||
// console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
|
||||
// console.log('finalCompletionStr: ', JSON.stringify(completionStr))
|
||||
|
||||
let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column)
|
||||
|
||||
return [{
|
||||
insertText: completionStr,
|
||||
range: rangeToReplace,
|
||||
}]
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// returns whether this autocompletion is in the cache
|
||||
// const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
|
||||
|
||||
// const originalPrefix = autocompletion.prefix
|
||||
// const generatedMiddle = autocompletion.result
|
||||
// const originalPrefixTrimmed = trimPrefix(originalPrefix)
|
||||
// const currentPrefixTrimmed = trimPrefix(prefix)
|
||||
|
||||
// if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed)
|
||||
// return isMatch
|
||||
|
||||
// }
|
||||
|
||||
const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
|
||||
|
||||
const fullText = model.getValue();
|
||||
|
||||
const cursorOffset = model.getOffsetAt(position)
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
const suffix = fullText.substring(cursorOffset)
|
||||
|
||||
return { prefix, suffix }
|
||||
|
||||
}
|
||||
|
||||
const getIndex = (str: string, line: number, char: number) => {
|
||||
return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char;
|
||||
}
|
||||
const getLastLine = (s: string): string => {
|
||||
const matches = s.match(/[^\n]*$/)
|
||||
return matches ? matches[0] : ''
|
||||
}
|
||||
|
||||
type matchInfo = {
|
||||
lineStart: number,
|
||||
character: number,
|
||||
startIdx: number,
|
||||
}
|
||||
// returns the startIdx of the match if there is a match, or undefined if there is no match
|
||||
// all results are wrt `autocompletion.result`
|
||||
const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => {
|
||||
|
||||
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix)
|
||||
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix)
|
||||
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText)
|
||||
|
||||
// console.log('@result: ', JSON.stringify(autocompletion.insertText))
|
||||
// console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix))
|
||||
// console.log('@trimmedCompletionPrefix: ', JSON.stringify(trimmedCompletionPrefix))
|
||||
// console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle))
|
||||
|
||||
if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time
|
||||
console.log('@undefined1')
|
||||
return undefined
|
||||
}
|
||||
|
||||
if ( // check that completion starts with the prefix
|
||||
!(trimmedCompletionPrefix + trimmedCompletionMiddle)
|
||||
.startsWith(trimmedCurrentPrefix)
|
||||
) {
|
||||
console.log('@undefined2')
|
||||
return undefined
|
||||
}
|
||||
|
||||
// reverse map to find position wrt `autocompletion.result`
|
||||
const lineStart =
|
||||
trimmedCurrentPrefix.split('\n').length -
|
||||
trimmedCompletionPrefix.split('\n').length;
|
||||
|
||||
if (lineStart < 0) {
|
||||
console.log('@undefined3')
|
||||
|
||||
console.error('Error: No line found.');
|
||||
return undefined;
|
||||
}
|
||||
const currentPrefixLine = getLastLine(trimmedCurrentPrefix)
|
||||
const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : ''
|
||||
const completionMiddleLine = autocompletion.insertText.split('\n')[lineStart]
|
||||
const fullCompletionLine = completionPrefixLine + completionMiddleLine
|
||||
|
||||
// console.log('currentPrefixLine', currentPrefixLine)
|
||||
// console.log('completionPrefixLine', completionPrefixLine)
|
||||
// console.log('completionMiddleLine', completionMiddleLine)
|
||||
|
||||
const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine)
|
||||
if (charMatchIdx < 0) {
|
||||
console.log('@undefined4', charMatchIdx)
|
||||
|
||||
console.error('Warning: Found character with negative index. This should never happen.')
|
||||
return undefined
|
||||
}
|
||||
|
||||
const character = (charMatchIdx +
|
||||
currentPrefixLine.length
|
||||
- completionPrefixLine.length
|
||||
)
|
||||
|
||||
const startIdx = getIndex(autocompletion.insertText, lineStart, character)
|
||||
|
||||
return {
|
||||
lineStart,
|
||||
character,
|
||||
startIdx,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
|
||||
|
||||
const prefixLines = prefix.split('\n')
|
||||
const suffixLines = suffix.split('\n')
|
||||
|
||||
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
|
||||
const suffixToRightOfCursor = suffixLines[0] ?? ''
|
||||
|
||||
// default parameters
|
||||
let shouldGenerate = true
|
||||
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
|
||||
|
||||
// specific cases
|
||||
if (suffixToRightOfCursor.trim() !== '') { // typing between something
|
||||
stopTokens = ['\n', '\r\n']
|
||||
}
|
||||
|
||||
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
|
||||
// stopTokens = ['\n\n', '\r\n\r\n']
|
||||
// }
|
||||
|
||||
if (prefixToLeftOfCursor === '') { // at beginning or end of line
|
||||
shouldGenerate = false
|
||||
}
|
||||
|
||||
return { shouldGenerate, stopTokens }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export interface IAutocompleteService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAutocompleteService = createDecorator<IAutocompleteService>('AutocompleteService');
|
||||
|
||||
export class AutocompleteService extends Disposable implements IAutocompleteService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _autocompletionId: number = 0;
|
||||
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
|
||||
|
||||
private _lastCompletionTime = 0
|
||||
private _lastPrefix: string = ''
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke and returns the completion to show
|
||||
async _provideInlineCompletionItems(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
context: InlineCompletionContext,
|
||||
token: CancellationToken,
|
||||
): Promise<InlineCompletion[]> {
|
||||
|
||||
const disabled = true
|
||||
const testMode = false
|
||||
|
||||
if (disabled) return [];
|
||||
|
||||
const docUriStr = model.uri.toString();
|
||||
|
||||
const { prefix, suffix } = getPrefixAndSuffix(model, position)
|
||||
// initialize cache and other variables
|
||||
// note that whenever an autocompletion is rejected, it is removed from cache
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) {
|
||||
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>(
|
||||
MAX_CACHE_SIZE,
|
||||
(autocompletion: Autocompletion) => {
|
||||
if (autocompletion.requestId)
|
||||
this._llmMessageService.abort(autocompletion.requestId)
|
||||
}
|
||||
)
|
||||
}
|
||||
this._lastPrefix = prefix
|
||||
|
||||
// print all pending autocompletions
|
||||
// let _numPending = 0
|
||||
// this._autocompletionsOfDocument[docUriStr].items.forEach((a: Autocompletion) => { if (a.status === 'pending') _numPending += 1 })
|
||||
// console.log('@numPending: ' + _numPending)
|
||||
|
||||
// get autocompletion from cache
|
||||
let cachedAutocompletion: Autocompletion | undefined = undefined
|
||||
let matchInfo: matchInfo | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
|
||||
// if the user's change matches up with the generated text
|
||||
matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion })
|
||||
if (matchInfo !== undefined) {
|
||||
cachedAutocompletion = autocompletion
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a cached autocompletion, return it
|
||||
if (cachedAutocompletion && matchInfo) {
|
||||
|
||||
// console.log('id: ' + cachedAutocompletion.id)
|
||||
|
||||
if (cachedAutocompletion.status === 'finished') {
|
||||
// console.log('A1')
|
||||
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true })
|
||||
return inlineCompletions
|
||||
|
||||
} else if (cachedAutocompletion.status === 'pending') {
|
||||
// console.log('A2')
|
||||
|
||||
try {
|
||||
await cachedAutocompletion.llmPromise;
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id)
|
||||
console.error('Error creating autocompletion (1): ' + e)
|
||||
}
|
||||
|
||||
} else if (cachedAutocompletion.status === 'error') {
|
||||
// console.log('A3')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// else if no more typing happens, then go forwards with the request
|
||||
// wait DEBOUNCE_TIME for the user to stop typing
|
||||
const thisTime = Date.now()
|
||||
this._lastCompletionTime = thisTime
|
||||
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this._lastCompletionTime === thisTime) {
|
||||
resolve(false)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
}, DEBOUNCE_TIME)
|
||||
)
|
||||
|
||||
// if more typing happened, then do not go forwards with the request
|
||||
if (didTypingHappenDuringDebounce) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
// if there are too many pending requests, cancel the oldest one
|
||||
let numPending = 0
|
||||
let oldestPending: Autocompletion | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
|
||||
if (autocompletion.status === 'pending') {
|
||||
numPending += 1
|
||||
if (oldestPending === undefined) {
|
||||
oldestPending = autocompletion
|
||||
}
|
||||
if (numPending >= MAX_PENDING_REQUESTS) {
|
||||
// cancel the oldest pending request and remove it from cache
|
||||
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { shouldGenerate, stopTokens: _ } = getCompletionOptions({ prefix, suffix }) // TODO mat
|
||||
|
||||
if (!shouldGenerate) return []
|
||||
|
||||
if (testMode && this._autocompletionId !== 0) { // TODO remove this
|
||||
return []
|
||||
}
|
||||
|
||||
// console.log('B')
|
||||
|
||||
// create a new autocompletion and add it to cache
|
||||
const newAutocompletion: Autocompletion = {
|
||||
id: this._autocompletionId++,
|
||||
prefix: prefix,
|
||||
suffix: suffix,
|
||||
startTime: Date.now(),
|
||||
endTime: undefined,
|
||||
status: 'pending',
|
||||
llmPromise: undefined,
|
||||
insertText: '',
|
||||
requestId: null,
|
||||
}
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
const requestId = this._llmMessageService.sendLLMMessage({
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
messages: [],
|
||||
onText: async ({ newText, fullText }) => {
|
||||
|
||||
newAutocompletion.insertText = fullText
|
||||
|
||||
// if generation doesn't match the prefix for the first few tokens generated, reject it
|
||||
if (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
reject('LLM response did not match user\'s text.')
|
||||
}
|
||||
},
|
||||
onFinalMessage: ({ fullText }) => {
|
||||
|
||||
// newAutocompletion.prefix = prefix
|
||||
// newAutocompletion.suffix = suffix
|
||||
// newAutocompletion.startTime = Date.now()
|
||||
newAutocompletion.endTime = Date.now()
|
||||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText))
|
||||
|
||||
resolve(newAutocompletion.insertText)
|
||||
|
||||
},
|
||||
onError: ({ message }) => {
|
||||
newAutocompletion.endTime = Date.now()
|
||||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
featureName: 'Autocomplete',
|
||||
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
// if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
|
||||
setTimeout(() => {
|
||||
if (newAutocompletion.status === 'pending') {
|
||||
reject('Timeout receiving message to LLM.')
|
||||
}
|
||||
}, TIMEOUT_TIME)
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
// add autocompletion to cache
|
||||
this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion)
|
||||
|
||||
// show autocompletion
|
||||
try {
|
||||
await newAutocompletion.llmPromise
|
||||
// console.log('id: ' + newAutocompletion.id)
|
||||
|
||||
const matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 }
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
|
||||
console.error('Error creating autocompletion (2): ' + e)
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
provideInlineCompletions: async (model, position, context, token) => {
|
||||
const items = await this._provideInlineCompletionItems(model, position, context, token)
|
||||
|
||||
// console.log('item: ', items?.[0]?.insertText)
|
||||
return { items: items, }
|
||||
},
|
||||
freeInlineCompletions: (completions) => {
|
||||
|
||||
// get the `docUriStr` and the `position` of the cursor
|
||||
const activePane = this._editorService.activeEditorPane;
|
||||
if (!activePane) return;
|
||||
const control = activePane.getControl();
|
||||
if (!control || !isCodeEditor(control)) return;
|
||||
const position = control.getPosition();
|
||||
if (!position) return;
|
||||
const resource = EditorResourceAccessor.getCanonicalUri(this._editorService.activeEditor);
|
||||
if (!resource) return;
|
||||
const model = this._modelService.getModel(resource)
|
||||
if (!model) return;
|
||||
const docUriStr = resource.toString();
|
||||
|
||||
const { prefix, } = getPrefixAndSuffix(model, position)
|
||||
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) return;
|
||||
|
||||
// go through cached items and remove matching ones
|
||||
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
|
||||
completions.items.forEach(item => {
|
||||
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
|
||||
if (removeLeftTabsAndTrimEnd(prefix)
|
||||
=== removeLeftTabsAndTrimEnd(autocompletion.prefix + autocompletion.insertText)
|
||||
) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,178 @@
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
|
||||
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
// lets you add a "consistent" item to a Model (aka URI), instead of just to a single editor
|
||||
|
||||
|
||||
type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); }
|
||||
|
||||
export interface IConsistentItemService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getEditorsOnURI(uri: URI): ICodeEditor[];
|
||||
addConsistentItemToURI(inputs: AddItemInputs): string;
|
||||
removeConsistentItemFromURI(consistentItemId: string): void;
|
||||
}
|
||||
|
||||
export const IConsistentItemService = createDecorator<IConsistentItemService>('ConsistentItemService');
|
||||
|
||||
export class ConsistentItemService extends Disposable {
|
||||
|
||||
readonly _serviceBrand: undefined
|
||||
|
||||
// the items that are attached to each URI, completely independent from current state of editors
|
||||
private readonly consistentItemIdsOfURI: Record<string, Set<string> | undefined> = {}
|
||||
private readonly infoOfConsistentItemId: Record<string, AddItemInputs> = {}
|
||||
|
||||
|
||||
// current state of items on each editor, and the fns to call to remove them
|
||||
private readonly itemIdsOfEditorId: Record<string, Set<string> | undefined> = {}
|
||||
private readonly consistentItemIdOfItemId: Record<string, string> = {}
|
||||
private readonly disposeFnOfItemId: Record<string, () => void> = {}
|
||||
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
|
||||
const removeItemsFromEditor = (editor: ICodeEditor) => {
|
||||
const editorId = editor.getId()
|
||||
for (const itemId of this.itemIdsOfEditorId[editorId] ?? [])
|
||||
this._removeItemFromEditor(editor, itemId)
|
||||
}
|
||||
|
||||
// put items on the editor, based on the consistent items for that URI
|
||||
const putItemsOnEditor = (editor: ICodeEditor, uri: URI | null) => {
|
||||
if (!uri) return
|
||||
for (const consistentItemId of this.consistentItemIdsOfURI[uri.fsPath] ?? [])
|
||||
this._putItemOnEditor(editor, consistentItemId)
|
||||
}
|
||||
|
||||
|
||||
const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
this._register(
|
||||
editor.onDidChangeModel(e => {
|
||||
removeItemsFromEditor(editor)
|
||||
putItemsOnEditor(editor, e.newModelUrl)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const addDisposeListener = (editor: ICodeEditor) => {
|
||||
this._register(editor.onDidDispose(() => {
|
||||
// anything on the editor has been disposed already
|
||||
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? [])
|
||||
delete this.disposeFnOfItemId[itemId]
|
||||
}))
|
||||
}
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
addTabSwitchListeners(editor)
|
||||
addDisposeListener(editor)
|
||||
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
|
||||
}
|
||||
|
||||
// initialize current editors + any new editors
|
||||
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
|
||||
// when an editor is deleted, remove its items
|
||||
this._register(this._editorService.onCodeEditorRemove(editor => {
|
||||
removeItemsFromEditor(editor)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
_putItemOnEditor(editor: ICodeEditor, consistentItemId: string) {
|
||||
const { fn } = this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
// add item
|
||||
const dispose = fn(editor)
|
||||
|
||||
const itemId = generateUuid()
|
||||
const editorId = editor.getId()
|
||||
|
||||
if (!(editorId in this.itemIdsOfEditorId))
|
||||
this.itemIdsOfEditorId[editorId] = new Set()
|
||||
this.itemIdsOfEditorId[editorId]!.add(itemId)
|
||||
|
||||
|
||||
this.consistentItemIdOfItemId[itemId] = consistentItemId
|
||||
|
||||
this.disposeFnOfItemId[itemId] = () => {
|
||||
// console.log('calling remove for', itemId)
|
||||
dispose?.()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
_removeItemFromEditor(editor: ICodeEditor, itemId: string) {
|
||||
|
||||
const editorId = editor.getId()
|
||||
this.itemIdsOfEditorId[editorId]?.delete(itemId)
|
||||
|
||||
this.disposeFnOfItemId[itemId]?.()
|
||||
delete this.disposeFnOfItemId[itemId]
|
||||
|
||||
delete this.consistentItemIdOfItemId[itemId]
|
||||
}
|
||||
|
||||
getEditorsOnURI(uri: URI) {
|
||||
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
|
||||
return editors
|
||||
}
|
||||
|
||||
consistentItemIdPool = 0
|
||||
addConsistentItemToURI({ uri, fn }: AddItemInputs) {
|
||||
const consistentItemId = (this.consistentItemIdPool++) + ''
|
||||
|
||||
if (!(uri.fsPath in this.consistentItemIdsOfURI))
|
||||
this.consistentItemIdsOfURI[uri.fsPath] = new Set()
|
||||
this.consistentItemIdsOfURI[uri.fsPath]!.add(consistentItemId)
|
||||
|
||||
this.infoOfConsistentItemId[consistentItemId] = { fn, uri }
|
||||
|
||||
const editors = this.getEditorsOnURI(uri)
|
||||
for (const editor of editors)
|
||||
this._putItemOnEditor(editor, consistentItemId)
|
||||
|
||||
return consistentItemId
|
||||
}
|
||||
|
||||
|
||||
removeConsistentItemFromURI(consistentItemId: string) {
|
||||
|
||||
if (!(consistentItemId in this.infoOfConsistentItemId))
|
||||
return
|
||||
|
||||
const { uri } = this.infoOfConsistentItemId[consistentItemId]
|
||||
const editors = this.getEditorsOnURI(uri)
|
||||
|
||||
for (const editor of editors) {
|
||||
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? []) {
|
||||
if (this.consistentItemIdOfItemId[itemId] === consistentItemId)
|
||||
this._removeItemFromEditor(editor, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
// clear
|
||||
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
|
||||
delete this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager);
|
||||
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines } from './react/out/util/diffLines.js'
|
||||
import { diffLines } from '../react/out/diff/index.js'
|
||||
|
||||
export type ComputedDiff = {
|
||||
type: 'edit';
|
||||
+5
-3
@@ -1,12 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OperatingSystem, OS } from '../../../../base/common/platform.js';
|
||||
import { isMacintosh } from '../../../../../base/common/platform.js';
|
||||
|
||||
// import { OperatingSystem, OS } from '../../../../base/common/platform.js';
|
||||
// OS === OperatingSystem.Macintosh
|
||||
export function getCmdKey(): string {
|
||||
if (OS === OperatingSystem.Macintosh) {
|
||||
if (isMacintosh) {
|
||||
return '⌘';
|
||||
} else {
|
||||
return 'Ctrl';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,9 @@
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
background-color: var(--vscode-void-sweepIdxBG);
|
||||
}
|
||||
|
||||
@@ -6,6 +11,10 @@
|
||||
background-color: var(--vscode-void-sweepBG);
|
||||
}
|
||||
|
||||
.void-highlightBG {
|
||||
background-color: var(--vscode-void-highlightBG);
|
||||
}
|
||||
|
||||
.void-greenBG {
|
||||
background-color: var(--vscode-void-greenBG);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,641 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { CodeSelection } from '../threadHistoryService.js';
|
||||
|
||||
export const chat_systemMessage = `\
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
Instructions:
|
||||
1. Output the changes to make to the entire file.
|
||||
1. Do not re-write the entire file.
|
||||
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
|
||||
4. You must give enough context to apply the change in the correct location.
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
FILES
|
||||
selected file \`math.ts\`:
|
||||
\`\`\`
|
||||
const addNumbers = (a, b) => a + b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
add a function that multiplies numbers below this
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
\`\`\`
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b;
|
||||
const multiplyNumbers = (a, b) => a * b;
|
||||
// existing code...
|
||||
\`\`\`
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
FILES
|
||||
selected file \`fib.ts\`:
|
||||
\`\`\`
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
console.log(root.val);
|
||||
dfs(root.left);
|
||||
dfs(root.right);
|
||||
}
|
||||
const fib = (n) => {
|
||||
if (n < 1) return 1
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
memoize results
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
\`\`\`
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
if (memo[n]) return memo[n]; // Check if result is already computed
|
||||
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
|
||||
return memo[n];
|
||||
}
|
||||
\`\`\`
|
||||
Explanation:
|
||||
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
|
||||
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
|
||||
Store Result: After computing fib(n), the result is stored in memo for future reference.
|
||||
|
||||
## END EXAMPLES\
|
||||
`
|
||||
|
||||
|
||||
|
||||
const stringifySelections = (selections: CodeSelection[]) => {
|
||||
return selections.map(({ fileURI, content, selectionStr }) =>
|
||||
`\
|
||||
File: ${fileURI.fsPath}
|
||||
\`\`\`
|
||||
${content // this was the enite file which is foolish
|
||||
}
|
||||
\`\`\`${selectionStr === null ? '' : `
|
||||
Selection: ${selectionStr}`}
|
||||
`).join('\n')
|
||||
}
|
||||
|
||||
|
||||
export const chat_prompt = (instructions: string, selections: CodeSelection[] | null) => {
|
||||
let str = '';
|
||||
if (selections && selections.length > 0) {
|
||||
str += stringifySelections(selections);
|
||||
str += `Please edit the selected code following these instructions:\n`
|
||||
}
|
||||
str += `${instructions}`;
|
||||
return str;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const ctrlLStream_systemMessage = `
|
||||
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
|
||||
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
|
||||
|
||||
Directions:
|
||||
1. Continue exactly where the new file \`new_file\` left off.
|
||||
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
|
||||
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
|
||||
|
||||
# Example 1:
|
||||
|
||||
ORIGINAL_FILE
|
||||
\`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
|
||||
NEW_FILE
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
\`\`\`
|
||||
|
||||
COMPLETION
|
||||
\`\`\`
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<div
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;\`\`\`
|
||||
`
|
||||
|
||||
|
||||
|
||||
|
||||
export const ctrlLStream_prompt = ({ originalCode, userMessage }: { originalCode: string, userMessage: string }) => {
|
||||
return `\
|
||||
ORIGINAL_CODE
|
||||
\`\`\`
|
||||
${originalCode}
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
${userMessage}
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const ctrlKStream_systemMessage = `\
|
||||
`
|
||||
|
||||
|
||||
export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
|
||||
|
||||
const fullFileLines = fullFileStr.split('\n')
|
||||
|
||||
// we can optimize this later
|
||||
const MAX_CHARS = 1024
|
||||
/*
|
||||
|
||||
a
|
||||
a
|
||||
a <-- final i (prefix = a\na\n)
|
||||
a
|
||||
|b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up)
|
||||
c
|
||||
d| <-- endLine-1 <-- initial j (moves down)
|
||||
e
|
||||
e <-- final j (suffix = e\ne\n)
|
||||
e
|
||||
e
|
||||
*/
|
||||
|
||||
let prefix = ''
|
||||
let i = startLine - 1 // 0-indexed exclusive
|
||||
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
|
||||
while (i !== 0) {
|
||||
const newLine = fullFileLines[i - 1]
|
||||
if (newLine.length + 1 + prefix.length <= MAX_CHARS) { // +1 to include the \n
|
||||
prefix = `${newLine}\n${prefix}`
|
||||
i -= 1
|
||||
}
|
||||
else break
|
||||
}
|
||||
|
||||
let suffix = ''
|
||||
let j = endLine - 1
|
||||
while (j !== fullFileLines.length - 1) {
|
||||
const newLine = fullFileLines[j + 1]
|
||||
if (newLine.length + 1 + suffix.length <= MAX_CHARS) { // +1 to include the \n
|
||||
suffix = `${suffix}\n${newLine}`
|
||||
j += 1
|
||||
}
|
||||
else break
|
||||
}
|
||||
|
||||
return { prefix, suffix }
|
||||
|
||||
}
|
||||
|
||||
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: { selection: string, prefix: string, suffix: string, userMessage: string, }) => {
|
||||
const onlySpeaksFIM = false
|
||||
|
||||
if (onlySpeaksFIM) {
|
||||
const preTag = 'PRE'
|
||||
const sufTag = 'SUF'
|
||||
const midTag = 'MID'
|
||||
return `\
|
||||
<${preTag}>
|
||||
/* Original Selection:
|
||||
${selection}*/
|
||||
/* Instructions: ${userMessage}*/
|
||||
${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
}
|
||||
// prompt the model on how to do FIM
|
||||
else {
|
||||
const preTag = 'PRE'
|
||||
const sufTag = 'SUF'
|
||||
const midTag = 'MID'
|
||||
return `\
|
||||
Here is the user's original selection:
|
||||
\`\`\`
|
||||
<${midTag}>${selection}</${midTag}>
|
||||
\`\`\`
|
||||
|
||||
The user wants to apply the following instructions to the selection:
|
||||
${userMessage}
|
||||
|
||||
Please rewrite the selection following the user's instructions.
|
||||
|
||||
Instructions to follow:
|
||||
1. Follow the user's instructions
|
||||
2. You may ONLY CHANGE the selection, and nothing else in the file
|
||||
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
|
||||
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
|
||||
|
||||
Complete the following:
|
||||
<${preTag}>${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// export const searchDiffChunkInstructions = `
|
||||
// You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
|
||||
|
||||
// Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
|
||||
|
||||
// # Example 1:
|
||||
|
||||
// FILES
|
||||
// selected file \`Sidebar.tsx\`:
|
||||
// \`\`\`
|
||||
// import React from 'react';
|
||||
// import styles from './Sidebar.module.css';
|
||||
|
||||
// interface SidebarProps {
|
||||
// items: { label: string; href: string }[];
|
||||
// onItemSelect?: (label: string) => void;
|
||||
// onExtraButtonClick?: () => void;
|
||||
// }
|
||||
|
||||
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
// return (
|
||||
// <div className={styles.sidebar}>
|
||||
// <ul>
|
||||
// {items.map((item, index) => (
|
||||
// <li key={index}>
|
||||
// <button
|
||||
// className={styles.sidebarButton}
|
||||
// onClick={() => onItemSelect?.(item.label)}
|
||||
// >
|
||||
// {item.label}
|
||||
// </button>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// Extra Action
|
||||
// </button>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default Sidebar;
|
||||
// \`\`\`
|
||||
|
||||
// DIFF
|
||||
// \`\`\`
|
||||
// @@ ... @@
|
||||
// -<div className={styles.sidebar}>
|
||||
// -<ul>
|
||||
// - {items.map((item, index) => (
|
||||
// - <li key={index}>
|
||||
// - <button
|
||||
// - className={styles.sidebarButton}
|
||||
// - onClick={() => onItemSelect?.(item.label)}
|
||||
// - >
|
||||
// - {item.label}
|
||||
// - </button>
|
||||
// - </li>
|
||||
// - ))}
|
||||
// -</ul>
|
||||
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// - Extra Action
|
||||
// -</button>
|
||||
// -</div>
|
||||
// +<div className={styles.sidebar}>
|
||||
// +<ul>
|
||||
// + {items.map((item, index) => (
|
||||
// + <li key={index}>
|
||||
// + <div
|
||||
// + className={styles.sidebarButton}
|
||||
// + onClick={() => onItemSelect?.(item.label)}
|
||||
// + >
|
||||
// + {item.label}
|
||||
// + </div>
|
||||
// + </li>
|
||||
// + ))}
|
||||
// +</ul>
|
||||
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// + Extra Action
|
||||
// +</div>
|
||||
// +</div>
|
||||
// \`\`\`
|
||||
|
||||
// SELECTION
|
||||
// \`\`\`
|
||||
// import React from 'react';
|
||||
// import styles from './Sidebar.module.css';
|
||||
|
||||
// interface SidebarProps {
|
||||
// items: { label: string; href: string }[];
|
||||
// onItemSelect?: (label: string) => void;
|
||||
// onExtraButtonClick?: () => void;
|
||||
// }
|
||||
|
||||
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
// return (
|
||||
// <div className={styles.sidebar}>
|
||||
// <ul>
|
||||
// {items.map((item, index) => (
|
||||
// \`\`\`
|
||||
|
||||
// RESULT
|
||||
// The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
|
||||
|
||||
// OUTPUT
|
||||
// \`true\`
|
||||
// `
|
||||
|
||||
|
||||
|
||||
// export const generateDiffInstructions = `
|
||||
// You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
// Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
// All changes made to files must be outputted in unified diff format.
|
||||
// Unified diff format instructions:
|
||||
// 1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
|
||||
// 2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
|
||||
// 3. Make diffs more than a few lines.
|
||||
// 4. Make high-level diffs rather than many one-line diffs.
|
||||
|
||||
// Here's an example of unified diff format:
|
||||
|
||||
// \`\`\`
|
||||
// @@ ... @@
|
||||
// -def factorial(n):
|
||||
// - if n == 0:
|
||||
// - return 1
|
||||
// - else:
|
||||
// - return n * factorial(n-1)
|
||||
// +def factorial(number):
|
||||
// + if number == 0:
|
||||
// + return 1
|
||||
// + else:
|
||||
// + return number * factorial(number-1)
|
||||
// \`\`\`
|
||||
|
||||
// Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
|
||||
|
||||
// \`\`\`
|
||||
// @@ ... @@ # This is less preferred because edits are close together and should be grouped:
|
||||
// -def factorial(n):
|
||||
// +def factorial(number):
|
||||
// - if n == 0:
|
||||
// + if number == 0:
|
||||
// return 1
|
||||
// else:
|
||||
// - return n * factorial(n-1)
|
||||
// + return number * factorial(number-1)
|
||||
// \`\`\`
|
||||
|
||||
// # Example 1:
|
||||
|
||||
// FILES
|
||||
// selected file \`test.ts\`:
|
||||
// \`\`\`
|
||||
// x = 1
|
||||
|
||||
// {{selection}}
|
||||
|
||||
// z = 3
|
||||
// \`\`\`
|
||||
|
||||
// SELECTION
|
||||
// \`\`\`const y = 2\`\`\`
|
||||
|
||||
// INSTRUCTIONS
|
||||
// \`\`\`y = 3\`\`\`
|
||||
|
||||
// EXPECTED RESULT
|
||||
|
||||
// We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
|
||||
// \`\`\`
|
||||
// @@ ... @@
|
||||
// -x = 1
|
||||
// -
|
||||
// -y = 2
|
||||
// +x = 1
|
||||
// +
|
||||
// +y = 3
|
||||
// \`\`\`
|
||||
|
||||
// # Example 2:
|
||||
|
||||
// FILES
|
||||
// selected file \`Sidebar.tsx\`:
|
||||
// \`\`\`
|
||||
// import React from 'react';
|
||||
// import styles from './Sidebar.module.css';
|
||||
|
||||
// interface SidebarProps {
|
||||
// items: { label: string; href: string }[];
|
||||
// onItemSelect?: (label: string) => void;
|
||||
// onExtraButtonClick?: () => void;
|
||||
// }
|
||||
|
||||
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
// return (
|
||||
// <div className={styles.sidebar}>
|
||||
// <ul>
|
||||
// {items.map((item, index) => (
|
||||
// <li key={index}>
|
||||
// {{selection}}
|
||||
// className={styles.sidebarButton}
|
||||
// onClick={() => onItemSelect?.(item.label)}
|
||||
// >
|
||||
// {item.label}
|
||||
// </button>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// Extra Action
|
||||
// </button>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default Sidebar;
|
||||
// \`\`\`
|
||||
|
||||
// SELECTION
|
||||
// \`\`\` <button\`\`\`
|
||||
|
||||
// INSTRUCTIONS
|
||||
// \`\`\`make all the buttons like this into divs\`\`\`
|
||||
|
||||
// EXPECTED OUTPUT
|
||||
|
||||
// We should change all the buttons like the one selected into a div component. Here is the change:
|
||||
// \`\`\`
|
||||
// @@ ... @@
|
||||
// -<div className={styles.sidebar}>
|
||||
// -<ul>
|
||||
// - {items.map((item, index) => (
|
||||
// - <li key={index}>
|
||||
// - <button
|
||||
// - className={styles.sidebarButton}
|
||||
// - onClick={() => onItemSelect?.(item.label)}
|
||||
// - >
|
||||
// - {item.label}
|
||||
// - </button>
|
||||
// - </li>
|
||||
// - ))}
|
||||
// -</ul>
|
||||
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// - Extra Action
|
||||
// -</button>
|
||||
// -</div>
|
||||
// +<div className={styles.sidebar}>
|
||||
// +<ul>
|
||||
// + {items.map((item, index) => (
|
||||
// + <li key={index}>
|
||||
// + <div
|
||||
// + className={styles.sidebarButton}
|
||||
// + onClick={() => onItemSelect?.(item.label)}
|
||||
// + >
|
||||
// + {item.label}
|
||||
// + </div>
|
||||
// + </li>
|
||||
// + ))}
|
||||
// +</ul>
|
||||
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// + Extra Action
|
||||
// +</div>
|
||||
// +</div>
|
||||
// \`\`\`
|
||||
// `;
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
import { CodeSelection } from '../registerThreads.js';
|
||||
|
||||
export const filesStr = (selections: CodeSelection[]) => {
|
||||
|
||||
return selections.map(({ fileURI, content, selectionStr }) =>
|
||||
`\
|
||||
File: ${fileURI.fsPath}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\`${selectionStr === null ? '' : `
|
||||
Selection: ${selectionStr}`}
|
||||
`).join('\n')
|
||||
}
|
||||
|
||||
|
||||
export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => {
|
||||
let str = '';
|
||||
if (selections && selections.length > 0) {
|
||||
str += filesStr(selections);
|
||||
str += `Please edit the selected code following these instructions:\n`
|
||||
}
|
||||
str += `${instructions}`;
|
||||
return str;
|
||||
};
|
||||
@@ -1,411 +0,0 @@
|
||||
|
||||
|
||||
|
||||
// // used for ctrl+l
|
||||
// const partialGenerationInstructions = ``
|
||||
|
||||
|
||||
// // used for ctrl+k, autocomplete
|
||||
// const fimInstructions = ``
|
||||
|
||||
|
||||
export const generateDiffInstructions = `
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
All changes made to files must be outputted in unified diff format.
|
||||
Unified diff format instructions:
|
||||
1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
|
||||
2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
|
||||
3. Make diffs more than a few lines.
|
||||
4. Make high-level diffs rather than many one-line diffs.
|
||||
|
||||
Here's an example of unified diff format:
|
||||
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-def factorial(n):
|
||||
- if n == 0:
|
||||
- return 1
|
||||
- else:
|
||||
- return n * factorial(n-1)
|
||||
+def factorial(number):
|
||||
+ if number == 0:
|
||||
+ return 1
|
||||
+ else:
|
||||
+ return number * factorial(number-1)
|
||||
\`\`\`
|
||||
|
||||
Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
|
||||
|
||||
\`\`\`
|
||||
@@ ... @@ # This is less preferred because edits are close together and should be grouped:
|
||||
-def factorial(n):
|
||||
+def factorial(number):
|
||||
- if n == 0:
|
||||
+ if number == 0:
|
||||
return 1
|
||||
else:
|
||||
- return n * factorial(n-1)
|
||||
+ return number * factorial(number-1)
|
||||
\`\`\`
|
||||
|
||||
# Example 1:
|
||||
|
||||
FILES
|
||||
selected file \`test.ts\`:
|
||||
\`\`\`
|
||||
x = 1
|
||||
|
||||
{{selection}}
|
||||
|
||||
z = 3
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`const y = 2\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`y = 3\`\`\`
|
||||
|
||||
EXPECTED RESULT
|
||||
|
||||
We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-x = 1
|
||||
-
|
||||
-y = 2
|
||||
+x = 1
|
||||
+
|
||||
+y = 3
|
||||
\`\`\`
|
||||
|
||||
# Example 2:
|
||||
|
||||
FILES
|
||||
selected file \`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{{selection}}
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\` <button\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`make all the buttons like this into divs\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
|
||||
We should change all the buttons like the one selected into a div component. Here is the change:
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
|
||||
export const searchDiffChunkInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
|
||||
|
||||
Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
|
||||
|
||||
# Example 1:
|
||||
|
||||
FILES
|
||||
selected file \`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
\`\`\`
|
||||
|
||||
RESULT
|
||||
The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
|
||||
|
||||
OUTPUT
|
||||
\`true\`
|
||||
`
|
||||
|
||||
|
||||
export const writeFileWithDiffInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
|
||||
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
|
||||
|
||||
Directions:
|
||||
1. Continue exactly where the new file \`new_file\` left off.
|
||||
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
|
||||
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
|
||||
|
||||
# Example 1:
|
||||
|
||||
ORIGINAL_FILE
|
||||
\`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
|
||||
NEW_FILE
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
\`\`\`
|
||||
|
||||
COMPLETION
|
||||
\`\`\`
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<div
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;\`\`\`
|
||||
`
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IInlineDiffsService } from './inlineDiffsService.js';
|
||||
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
onGetInputBox: (i: InputBox) => void;
|
||||
onChangeHeight: (height: number) => void;
|
||||
onUserUpdateText: (text: string) => void;
|
||||
initText: string | null;
|
||||
}
|
||||
|
||||
export type QuickEdit = {
|
||||
startLine: number, // 0-indexed
|
||||
beforeCode: string,
|
||||
afterCode?: string,
|
||||
instructions?: string,
|
||||
responseText?: string, // model can produce a text response too
|
||||
}
|
||||
|
||||
|
||||
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor(
|
||||
) {
|
||||
super({
|
||||
id: VOID_CTRL_K_ACTION_ID,
|
||||
title: 'Void: Quick Edit',
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
|
||||
weight: KeybindingWeight.BuiltinExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
metricsService.capture('User Action', { type: 'Open Ctrl+K' })
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
if (!editor) return;
|
||||
const model = editor.getModel()
|
||||
if (!model) return;
|
||||
const selection = editor.getSelection()
|
||||
if (!selection) return;
|
||||
|
||||
|
||||
const { startLineNumber: startLine, endLineNumber: endLine } = selection
|
||||
|
||||
// deselect - clear selection
|
||||
editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 })
|
||||
|
||||
const inlineDiffsService = accessor.get(IInlineDiffsService)
|
||||
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { QuickEdit } from './quickEditActions.js';
|
||||
|
||||
|
||||
|
||||
// service that manages state
|
||||
export type VoidQuickEditState = {
|
||||
quickEditsOfDocument: { [uri: string]: QuickEdit }
|
||||
}
|
||||
|
||||
export interface IQuickEditStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: VoidQuickEditState; // readonly to the user
|
||||
setState(newState: Partial<VoidQuickEditState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
onDidFocusChat: Event<void>;
|
||||
onDidBlurChat: Event<void>;
|
||||
fireFocusChat(): void;
|
||||
fireBlurChat(): void;
|
||||
|
||||
}
|
||||
|
||||
export const IQuickEditStateService = createDecorator<IQuickEditStateService>('voidQuickEditStateService');
|
||||
class VoidQuickEditStateService extends Disposable implements IQuickEditStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidQuickEditStateService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onFocusChat = new Emitter<void>();
|
||||
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
|
||||
|
||||
private readonly _onBlurChat = new Emitter<void>();
|
||||
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
|
||||
|
||||
|
||||
// state
|
||||
state: VoidQuickEditState
|
||||
|
||||
constructor(
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
this.state = { quickEditsOfDocument: {} }
|
||||
}
|
||||
|
||||
|
||||
setState(newState: Partial<VoidQuickEditState>) {
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager);
|
||||
@@ -1,13 +1,65 @@
|
||||
import { execSync } from 'child_process';
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// clear temp dirs
|
||||
execSync('npx rimraf out/ && npx rimraf src2/')
|
||||
import { spawn, execSync } from 'child_process';
|
||||
|
||||
// build and scope tailwind
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "prefix-" ')
|
||||
const args = process.argv.slice(2);
|
||||
const isWatch = args.includes('--watch') || args.includes('-w');
|
||||
|
||||
// tsup to build src2/ into out/
|
||||
execSync('npx tsup')
|
||||
if (isWatch) {
|
||||
// Watch mode
|
||||
// Create a watcher for scope-tailwind using nodemon
|
||||
const scopeTailwindWatcher = spawn('npx', [
|
||||
'nodemon',
|
||||
'--watch', 'src',
|
||||
'--ext', 'ts,tsx,css',
|
||||
'--exec',
|
||||
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"'
|
||||
]);
|
||||
|
||||
// Create a watcher for tsup in watch mode
|
||||
const tsupWatcher = spawn('npx', [
|
||||
'tsup',
|
||||
'--watch'
|
||||
]);
|
||||
|
||||
console.log('✅ Done building! Press Cmd+Shift+B again.')
|
||||
// Handle scope-tailwind watcher output
|
||||
scopeTailwindWatcher.stdout.on('data', (data) => {
|
||||
console.log(`[scope-tailwind] ${data}`);
|
||||
});
|
||||
|
||||
scopeTailwindWatcher.stderr.on('data', (data) => {
|
||||
console.error(`[scope-tailwind] ${data}`);
|
||||
});
|
||||
|
||||
// Handle tsup watcher output
|
||||
tsupWatcher.stdout.on('data', (data) => {
|
||||
console.log(`[tsup] ${data}`);
|
||||
});
|
||||
|
||||
tsupWatcher.stderr.on('data', (data) => {
|
||||
console.error(`[tsup] ${data}`);
|
||||
});
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', () => {
|
||||
scopeTailwindWatcher.kill();
|
||||
tsupWatcher.kill();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
console.log('🔄 Watchers started! Press Ctrl+C to stop both watchers.');
|
||||
} else {
|
||||
// Build mode
|
||||
console.log('📦 Building...');
|
||||
|
||||
// Run scope-tailwind once
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"', { stdio: 'inherit' });
|
||||
|
||||
// Run tsup once
|
||||
execSync('npx tsup', { stdio: 'inherit' });
|
||||
|
||||
console.log('✅ Build complete!');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useIsDark, useSidebarState } from '../util/services.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { CtrlKChat } from './CtrlKChat.js'
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js'
|
||||
|
||||
export const CtrlK = (props: QuickEditPropsType) => {
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<ErrorBoundary>
|
||||
<CtrlKChat {...props} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
|
||||
import { OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { getCmdKey } from '../../../helpers/getCmdKey.js';
|
||||
import { VoidInputBox } from '../util/inputs.js';
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const inlineDiffsService = accessor.get('IInlineDiffsService')
|
||||
const sizerRef = useRef<HTMLDivElement | null>(null)
|
||||
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const inputContainer = sizerRef.current
|
||||
if (!inputContainer) return;
|
||||
|
||||
// only observing 1 element
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const height = entries[0].borderBoxSize[0].blockSize
|
||||
onChangeHeight(height)
|
||||
})
|
||||
resizeObserver.observe(inputContainer);
|
||||
|
||||
return () => { resizeObserver?.disconnect(); };
|
||||
}, [onChangeHeight]);
|
||||
|
||||
// state of current message
|
||||
const [instructions, setInstructions] = useState(initText ?? '') // the user's instructions
|
||||
const onChangeText = useCallback((newStr: string) => {
|
||||
setInstructions(newStr)
|
||||
onUserUpdateText(newStr)
|
||||
}, [setInstructions])
|
||||
const isDisabled = !instructions.trim()
|
||||
|
||||
const currentlyStreamingIdRef = useRef<number | undefined>(undefined)
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
|
||||
const onSubmit = useCallback((e: FormEvent) => {
|
||||
if (currentlyStreamingIdRef.current !== undefined) return
|
||||
inputBoxRef.current?.disable()
|
||||
|
||||
currentlyStreamingIdRef.current = inlineDiffsService.startApplying({
|
||||
featureName: 'Ctrl+K',
|
||||
diffareaid: diffareaid,
|
||||
userMessage: instructions,
|
||||
})
|
||||
setIsStreaming(true)
|
||||
}, [inlineDiffsService, diffareaid, instructions])
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (currentlyStreamingIdRef.current !== undefined)
|
||||
inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current)
|
||||
setIsStreaming(false)
|
||||
}, [inlineDiffsService])
|
||||
|
||||
|
||||
// sync init value
|
||||
const alreadySetRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (!inputBoxRef.current) return
|
||||
if (alreadySetRef.current) return
|
||||
alreadySetRef.current = true
|
||||
inputBoxRef.current.value = instructions
|
||||
}, [initText, instructions])
|
||||
|
||||
return <div className='py-2 w-full max-w-xl' ref={sizerRef}>
|
||||
<form
|
||||
// copied from SidebarChat.tsx
|
||||
className={`
|
||||
flex flex-col gap-2 p-1 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
`
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
return
|
||||
}
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
if (isDisabled) {
|
||||
// __TODO__ show disabled
|
||||
return
|
||||
}
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.currentTarget === e.target) {
|
||||
inputBoxRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
<div // this div is used to position the input box properly
|
||||
className={`w-full p-2 z-[999]`}
|
||||
>
|
||||
<div className='flex flex-row justify-between items-end gap-1'>
|
||||
{/* left (input) */}
|
||||
<div // copied from SidebarChat.tsx
|
||||
className={`w-full
|
||||
@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_div.monaco-inputbox]:!void-outline-none`}>
|
||||
{/* text input */}
|
||||
<VoidInputBox
|
||||
placeholder={`${getCmdKey()}+K to select`}
|
||||
onChangeText={onChangeText}
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
inputBoxRef.current = instance;
|
||||
onGetInputBox(instance);
|
||||
instance.focus()
|
||||
}, [onGetInputBox])}
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* right (button) */}
|
||||
<div className='flex flex-row items-end'>
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { CtrlK } from './CtrlK.js'
|
||||
|
||||
|
||||
export const mountCtrlK = mountFnGenerator(CtrlK)
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
export { diffLines, Change }
|
||||
@@ -1,28 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { ReactNode } from "react"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { VoidCodeEditor } from '../util/inputs.js';
|
||||
|
||||
|
||||
|
||||
export function getLanguageFromFileName(fileName: string): string {
|
||||
if (!fileName) return 'plaintext';
|
||||
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
if (!ext) return 'plaintext';
|
||||
|
||||
const extensionMap: { [key: string]: string } = {
|
||||
// Web
|
||||
'html': 'html',
|
||||
'htm': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'less': 'less',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
|
||||
// Programming Languages
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'cc': 'cpp',
|
||||
'h': 'cpp',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'sh': 'shell',
|
||||
'bash': 'shell',
|
||||
'zsh': 'shell',
|
||||
|
||||
// Markup/Config
|
||||
'md': 'markdown',
|
||||
'markdown': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Other
|
||||
'sql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile'
|
||||
};
|
||||
|
||||
return extensionMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
|
||||
const customStyle = {
|
||||
...atomOneDarkReasonable,
|
||||
'code[class*="language-"]': {
|
||||
...atomOneDarkReasonable['code[class*="language-"]'],
|
||||
background: "none",
|
||||
},
|
||||
}
|
||||
|
||||
return (<>
|
||||
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
|
||||
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className="flex space-x-2 p-2">{buttonsOnHover}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
<VoidCodeEditor
|
||||
initValue={text}
|
||||
language={language}
|
||||
/>
|
||||
{/* <div
|
||||
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg`}
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
@@ -33,7 +93,7 @@ export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, bu
|
||||
{text}
|
||||
</SyntaxHighlighter>
|
||||
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
+23
-18
@@ -1,7 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { JSX, useCallback, useEffect, useState } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { useService } from '../util/services.js'
|
||||
import { useAccessor } from '../util/services.js'
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
@@ -13,10 +18,13 @@ enum CopyButtonState {
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const inlineDiffService = useService('inlineDiffService')
|
||||
const accessor = useAccessor()
|
||||
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const inlineDiffService = accessor.get('IInlineDiffsService')
|
||||
const clipboardService = accessor.get('IClipboardService')
|
||||
useEffect(() => {
|
||||
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
setCopyButtonState(CopyButtonState.Copy)
|
||||
@@ -25,15 +33,10 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
}, [copyButtonState])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Copied)
|
||||
},
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Error)
|
||||
}
|
||||
)
|
||||
}, [text])
|
||||
clipboardService.writeText(text)
|
||||
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
|
||||
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
|
||||
}, [text, clipboardService])
|
||||
|
||||
return <>
|
||||
<button
|
||||
@@ -44,9 +47,11 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={async () => {
|
||||
|
||||
inlineDiffService.startStreaming('ctrl+l', text)
|
||||
onClick={() => {
|
||||
inlineDiffService.startApplying({
|
||||
featureName: 'Ctrl+L',
|
||||
userMessage: text,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@@ -127,7 +132,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
<MarkdownRender string={item.text} nested={true} />
|
||||
<ChatMarkdownRender string={item.text} nested={true} />
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
@@ -166,7 +171,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
|
||||
if (t.type === "link") {
|
||||
return (
|
||||
<a href={t.href} title={t.title ?? undefined}>
|
||||
<a className='underline' onClick={() => { window.open(t.href) }} href={t.href} title={t.title ?? undefined}>
|
||||
{t.text}
|
||||
</a>
|
||||
)
|
||||
@@ -211,7 +216,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
)
|
||||
}
|
||||
|
||||
export const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
@@ -0,0 +1,66 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
return {
|
||||
hasError: true,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError && this.state.error) {
|
||||
// If a custom fallback is provided, use it
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
// Use ErrorDisplay component as the default error UI
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={this.state.error + ''}
|
||||
fullError={this.state.error}
|
||||
onDismiss={this.props.onDismiss || null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
message,
|
||||
fullError,
|
||||
onDismiss,
|
||||
showDismiss,
|
||||
}: {
|
||||
message: string,
|
||||
fullError: Error | null,
|
||||
onDismiss: (() => void) | null,
|
||||
showDismiss?: boolean,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
let details: string | null = null;
|
||||
|
||||
if (fullError === null) {
|
||||
details = null
|
||||
}
|
||||
else if (typeof fullError === 'object') {
|
||||
details = JSON.stringify(fullError, null, 2)
|
||||
}
|
||||
else if (typeof fullError === 'string') {
|
||||
details = null
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-red-800">
|
||||
{/* eg Error */}
|
||||
Error
|
||||
</h3>
|
||||
<p className="text-red-700 mt-1">
|
||||
{/* eg Something went wrong */}
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{details && (
|
||||
<button className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-5 w-5" />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{showDismiss && onDismiss && (
|
||||
<button className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
onClick={onDismiss}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && details && (
|
||||
<div className="mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto">
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Full Error: </span>
|
||||
<pre className="text-red-700">{details}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
|
||||
import { SidebarSettings } from './SidebarSettings.js';
|
||||
import { useSidebarState } from '../util/services.js';
|
||||
// import { SidebarSettings } from './SidebarSettings.js';
|
||||
|
||||
|
||||
import { useIsDark, useSidebarState } from '../util/services.js';
|
||||
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
// import { SidebarChat } from './SidebarChat.js';
|
||||
|
||||
import '../styles.css'
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { SidebarChat } from './SidebarChat.js';
|
||||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
|
||||
const Sidebar = () => {
|
||||
export const Sidebar = ({ className }: { className: string }) => {
|
||||
const sidebarState = useSidebarState()
|
||||
const { isHistoryOpen, currentTab: tab } = sidebarState
|
||||
|
||||
return <div className='@@void-scope'>
|
||||
<div className={`flex flex-col h-screen w-full`}>
|
||||
const isDark = useIsDark()
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
|
||||
<div className={`flex flex-col px-2 py-2 w-full h-full`}>
|
||||
|
||||
{/* <span onClick={() => {
|
||||
const tabs = ['chat', 'settings', 'threadSelector']
|
||||
@@ -27,24 +31,31 @@ const Sidebar = () => {
|
||||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
<div className={`mb-2 h-[30vh] ${isHistoryOpen ? '' : 'hidden'}`}>
|
||||
<SidebarThreadSelector />
|
||||
<div className={`mb-2 w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarThreadSelector />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className={`${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<SidebarChat />
|
||||
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarChat />
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* <ErrorBoundary>
|
||||
<ModelSelectionSettings />
|
||||
</ErrorBoundary> */}
|
||||
</div>
|
||||
|
||||
<div className={`${tab === 'settings' ? '' : 'hidden'}`}>
|
||||
<SidebarSettings />
|
||||
</div>
|
||||
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings />
|
||||
</ErrorBoundary>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
const mountFn = mountFnGenerator(Sidebar)
|
||||
export default mountFn
|
||||
|
||||
|
||||
@@ -1,25 +1,190 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useConfigState, useService, useThreadsState } from '../util/services.js';
|
||||
import { generateDiffInstructions } from '../../../prompt/systemPrompts.js';
|
||||
import { userInstructionsStr } from '../../../prompt/stringifyFiles.js';
|
||||
import { CodeSelection, CodeStagingSelection } from '../../../registerThreads.js';
|
||||
import { useAccessor, useThreadsState } from '../util/services.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection, IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { MarkdownRender } from '../markdown/MarkdownRender.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { BlockCode, getLanguageFromFileName } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from '../util/ErrorDisplay.js';
|
||||
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { getCmdKey } from '../../../helpers/getCmdKey.js'
|
||||
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { VoidCodeEditor, VoidInputBox } from '../util/inputs.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
|
||||
|
||||
const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
d='M6 18 18 6M6 6l12 12'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const IconArrowUp = ({ size, className = '' }: { size: number, className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill="black"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.1918 8.90615C15.6381 8.45983 16.3618 8.45983 16.8081 8.90615L21.9509 14.049C22.3972 14.4953 22.3972 15.2189 21.9509 15.6652C21.5046 16.1116 20.781 16.1116 20.3347 15.6652L17.1428 12.4734V22.2857C17.1428 22.9169 16.6311 23.4286 15.9999 23.4286C15.3688 23.4286 14.8571 22.9169 14.8571 22.2857V12.4734L11.6652 15.6652C11.2189 16.1116 10.4953 16.1116 10.049 15.6652C9.60265 15.2189 9.60265 14.4953 10.049 14.049L15.1918 8.90615Z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const IconSquare = ({ size, className = '' }: { size: number, className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
stroke="black"
|
||||
fill="black"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="2" y="2" width="20" height="20" rx="4" ry="4" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const IconWarning = ({ size, className = '' }: { size: number, className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
height={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.56 1h.88l6.54 12.26-.44.74H1.44L1 13.26 7.56 1zM8 2.28L2.28 13H13.7L8 2.28zM8.625 12v-1h-1.25v1h1.25zm-1.25-2V6h1.25v4h-1.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
|
||||
const DEFAULT_BUTTON_SIZE = 20;
|
||||
export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required<Pick<ButtonProps, 'disabled'>>) => {
|
||||
|
||||
return <button
|
||||
type='submit'
|
||||
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
|
||||
${disabled ? 'bg-vscode-disabled-fg' : 'bg-white'}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2]" />
|
||||
</button>
|
||||
}
|
||||
|
||||
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
className={`rounded-full bg-white shrink-0 grow-0 cursor-pointer flex items-center justify-center
|
||||
${className}
|
||||
`}
|
||||
type='button'
|
||||
{...props}
|
||||
>
|
||||
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[6px]" />
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => {
|
||||
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (divRef.current) {
|
||||
divRef.current.scrollTop = divRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
const div = divRef.current;
|
||||
if (!div) return;
|
||||
|
||||
const isBottom = Math.abs(
|
||||
div.scrollHeight - div.clientHeight - div.scrollTop
|
||||
) < 4;
|
||||
|
||||
setIsAtBottom(isBottom);
|
||||
};
|
||||
|
||||
// When children change (new messages added)
|
||||
useEffect(() => {
|
||||
if (isAtBottom) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [children, isAtBottom]); // Dependency on children to detect new messages
|
||||
|
||||
// Initial scroll to bottom
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
// options={{ vertical: ScrollbarVisibility.Auto, horizontal: ScrollbarVisibility.Auto }}
|
||||
ref={divRef}
|
||||
onScroll={onScroll}
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// import { } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
// read files from VSCode
|
||||
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
@@ -29,27 +194,6 @@ const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string; // content sent to the llm
|
||||
displayContent: string; // content displayed to user
|
||||
selections: CodeSelection[] | null; // the user's selection
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
content: string; // content received from LLM
|
||||
displayContent: string | undefined; // content displayed to user (this is the same as content for now)
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const getBasename = (pathStr: string) => {
|
||||
// 'unixify' path
|
||||
pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with /
|
||||
@@ -62,63 +206,108 @@ export const SelectedFiles = (
|
||||
| { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined }
|
||||
| { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) }
|
||||
) => {
|
||||
|
||||
// index -> isOpened
|
||||
const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? [])
|
||||
|
||||
return (
|
||||
!!selections && selections.length !== 0 && (
|
||||
<div className='flex flex-wrap -mx-1 -mb-1'>
|
||||
{selections.map((selection, i) => (
|
||||
<Fragment key={i}>
|
||||
<div
|
||||
className='flex flex-wrap gap-2 text-left'
|
||||
>
|
||||
{selections.map((selection, i) => {
|
||||
|
||||
<button
|
||||
disabled={!setStaging}
|
||||
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={() => {
|
||||
if (type !== 'staging') return
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1, Infinity)])
|
||||
}}
|
||||
const showSelectionText = !!(selection.selectionStr && selectionIsOpened[i])
|
||||
|
||||
return (
|
||||
<div key={i} // container for `selectionSummary` and `selectionText`
|
||||
className={`${showSelectionText ? 'w-full' : ''}`}
|
||||
>
|
||||
<span>{getBasename(selection.fileURI.fsPath)}</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' && <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>
|
||||
{/* selection text */}
|
||||
{type === 'staging' && selection.selectionStr && <BlockCode text={selection.selectionStr}
|
||||
buttonsOnHover={(<button
|
||||
{/* selection summary */}
|
||||
<div
|
||||
// className="relative rounded rounded-e-2xl flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default"
|
||||
className={`flex items-center gap-1 relative
|
||||
rounded-md p-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
bg-vscode-editor-bg hover:brightness-95
|
||||
border border-vscode-commandcenter-border rounded-xs
|
||||
text-xs text-vscode-editor-fg text-nowrap
|
||||
`}
|
||||
onClick={() => {
|
||||
setStaging([...selections.slice(0, i), { ...selection, selectionStr: null }, ...selections.slice(i + 1, Infinity)])
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
}}
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
>Remove</button>
|
||||
)} />}
|
||||
</Fragment>
|
||||
))}
|
||||
>
|
||||
<span className=''>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' &&
|
||||
<span
|
||||
className='
|
||||
cursor-pointer
|
||||
bg-vscode-editorwidget-bg hover:bg-vscode-toolbar-hover-bg
|
||||
rounded-md
|
||||
z-1
|
||||
'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (type !== 'staging') return;
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3] text-vscode-toolbar-foreground" />
|
||||
</span>
|
||||
}
|
||||
|
||||
{/* type of selection */}
|
||||
{/* <span className='truncate'>{selection.selectionStr !== null ? 'Selection' : 'File'}</span> */}
|
||||
{/* X button */}
|
||||
{/* {type === 'staging' && // hoveredIdx === i
|
||||
<span className='absolute right-0 top-0 translate-x-[50%] translate-y-[-50%] cursor-pointer bg-white rounded-full border border-vscode-input-border z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (type !== 'staging') return;
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3]" />
|
||||
</span>
|
||||
} */}
|
||||
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{showSelectionText &&
|
||||
<div className='w-full p-1 rounded-sm border-vscode-editor-border bg-vscode-sidebar-bg'>
|
||||
<BlockCode text={selection.selectionStr!} language={getLanguageFromFileName(selection.fileURI.path)} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||
const ChatBubble = ({ chatMessage }: {
|
||||
chatMessage: ChatMessage
|
||||
}) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
const children = chatMessage.displayContent
|
||||
|
||||
if (!children)
|
||||
if (!chatMessage.displayContent)
|
||||
return null
|
||||
|
||||
let chatbubbleContents: React.ReactNode
|
||||
@@ -126,15 +315,15 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections} />
|
||||
{children}
|
||||
{chatMessage.displayContent}
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent} /> // 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`}>
|
||||
<div className={`text-left inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full overflow-auto`}>
|
||||
{chatbubbleContents}
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,46 +333,47 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||
|
||||
export const SidebarChat = () => {
|
||||
|
||||
const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
|
||||
|
||||
const modelService = useService('modelService')
|
||||
const accessor = useAccessor()
|
||||
const modelService = accessor.get('IModelService')
|
||||
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
const sidebarStateService = useService('sidebarStateService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
useEffect(() => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
sidebarStateService.onDidFocusChat(() => { chatInputRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { chatInputRef.current?.blur() })
|
||||
sidebarStateService.onDidFocusChat(() => { inputBoxRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { inputBoxRef.current?.blur() })
|
||||
)
|
||||
return () => disposables.forEach(d => d.dispose())
|
||||
}, [sidebarStateService, chatInputRef])
|
||||
|
||||
// config state
|
||||
const configState = useConfigState()
|
||||
const { voidConfig } = configState
|
||||
}, [sidebarStateService, inputBoxRef])
|
||||
|
||||
// threads state
|
||||
const threadsState = useThreadsState()
|
||||
const threadsStateService = useService('threadsStateService')
|
||||
const threadsStateService = accessor.get('IThreadHistoryService')
|
||||
|
||||
const llmMessageService = accessor.get('ILLMMessageService')
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
// state of current message
|
||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
|
||||
// state of chat
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [messageStream, setMessageStream] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const latestRequestIdRef = useRef<string | null>(null)
|
||||
|
||||
const [latestError, setLatestError] = useState<Error | string | null>(null)
|
||||
const [latestError, setLatestError] = useState<Parameters<OnError>[0] | null>(null)
|
||||
|
||||
const sendLLMMessageService = useService('sendLLMMessageService')
|
||||
|
||||
const isDisabled = !instructions
|
||||
// state of current message
|
||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
const isDisabled = !instructions.trim()
|
||||
const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead
|
||||
const [sidebarHeight, setSidebarHeight] = useState(0)
|
||||
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
|
||||
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
@@ -191,165 +381,235 @@ export const SidebarChat = () => {
|
||||
if (isLoading) return
|
||||
|
||||
|
||||
const currSelns = threadsStateService.state._currentStagingSelections
|
||||
|
||||
const currSelns = threadsStateService.state._currentStagingSelections ?? []
|
||||
const selections = !currSelns ? null : await Promise.all(
|
||||
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
|
||||
).then(
|
||||
(files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
)
|
||||
|
||||
|
||||
// // TODO don't save files to the thread history
|
||||
// const selectedSnippets = currSelns.filter(sel => sel.selectionStr !== null)
|
||||
// const selectedFiles = await Promise.all( // do not add these to the context history
|
||||
// currSelns.filter(sel => sel.selectionStr === null)
|
||||
// .map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
|
||||
// ).then(
|
||||
// (files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
// )
|
||||
// const contextToSendToLLM = ''
|
||||
// const contextToAddToHistory = ''
|
||||
|
||||
|
||||
// add system message to chat history
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: chat_systemMessage }
|
||||
threadsStateService.addMessageToCurrentThread(systemPromptElt)
|
||||
|
||||
const userContent = userInstructionsStr(instructions, selections)
|
||||
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selections }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
// add user's message to chat history
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
|
||||
threadsStateService.addMessageToCurrentThread(userHistoryElt)
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
|
||||
|
||||
|
||||
// send message to LLM
|
||||
setIsLoading(true) // must come before message is sent so onError will work
|
||||
setLatestError(null)
|
||||
if (inputBoxRef.current) {
|
||||
inputBoxRef.current.value = ''; // this triggers onDidChangeText
|
||||
inputBoxRef.current.blur();
|
||||
}
|
||||
|
||||
const object: LLMMessageServiceParams = {
|
||||
const object: ServiceSendLLMMessageParams = {
|
||||
logging: { loggingName: 'Chat' },
|
||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
|
||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content || '(null)' })),],
|
||||
onText: ({ newText, fullText }) => setMessageStream(fullText),
|
||||
onFinalMessage: ({ fullText: content }) => {
|
||||
console.log('chat: running final message')
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
setMessageStream('')
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
setMessageStream(null)
|
||||
setIsLoading(false)
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
console.log('chat: running error', error)
|
||||
onError: ({ message, fullError }) => {
|
||||
console.log('chat: running error', message, fullError)
|
||||
|
||||
// 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, }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
let content = messageStream ?? ''; // just use the current content
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null, }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
setLatestError(error)
|
||||
setLatestError({ message, fullError })
|
||||
},
|
||||
voidConfig,
|
||||
featureName: 'Ctrl+L',
|
||||
|
||||
}
|
||||
|
||||
const latestRequestId = sendLLMMessageService.sendLLMMessage(object)
|
||||
const latestRequestId = llmMessageService.sendLLMMessage(object)
|
||||
latestRequestIdRef.current = latestRequestId
|
||||
|
||||
|
||||
setIsLoading(true)
|
||||
setInstructions('');
|
||||
formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
|
||||
threadsStateService.setStaging([]) // clear staging
|
||||
setLatestError(null)
|
||||
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
// abort the LLM
|
||||
// abort the LLM call
|
||||
if (latestRequestIdRef.current)
|
||||
sendLLMMessageService.abort(latestRequestIdRef.current)
|
||||
llmMessageService.abort(latestRequestIdRef.current)
|
||||
|
||||
// if messageStream was not empty, add it to the history
|
||||
const llmContent = messageStream || '(null)'
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
const llmContent = messageStream ?? ''
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream || null, }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
}
|
||||
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread(threadsState)
|
||||
|
||||
const selections = threadsState._currentStagingSelections
|
||||
|
||||
return <>
|
||||
<div className="overflow-x-hidden space-y-4">
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
|
||||
// const [_test_messages, _set_test_messages] = useState<string[]>([])
|
||||
|
||||
return <div
|
||||
ref={(ref) => { if (ref) { setSidebarHeight(ref.clientHeight); } }}
|
||||
className={`w-full h-full`}
|
||||
>
|
||||
<ScrollToBottomContainer
|
||||
className={`overflow-x-hidden overflow-y-auto`}
|
||||
style={{ maxHeight: sidebarHeight - formHeight - 30 }}
|
||||
>
|
||||
{/* previous messages */}
|
||||
{currentThread !== null && currentThread?.messages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
{previousMessages.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">
|
||||
{/* selections */}
|
||||
{(selections && selections.length !== 0) && <div className="p-2 pb-0 space-y-2">
|
||||
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
|
||||
</div>}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} />
|
||||
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-row items-center rounded-md p-2"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||
{/* {_test_messages.map((_, i) => <div key={i}>div {i}</div>)}
|
||||
<div>{`totalHeight: ${sidebarHeight - formHeight - 30}`}</div>
|
||||
<div>{`sidebarHeight: ${sidebarHeight}`}</div>
|
||||
<div>{`formHeight: ${formHeight}`}</div>
|
||||
<button type='button' onClick={() => { _set_test_messages(d => [...d, 'asdasdsadasd']) }}>add div</button> */}
|
||||
|
||||
onSubmit={(e) => {
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}>
|
||||
{/* input */}
|
||||
</ScrollToBottomContainer>
|
||||
|
||||
<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>
|
||||
|
||||
{/* input box */}
|
||||
<div // this div is used to position the input box properly
|
||||
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
|
||||
>
|
||||
<form
|
||||
ref={(ref) => { if (ref) { setFormHeight(ref.clientHeight); } }}
|
||||
className={`
|
||||
flex flex-col gap-2 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
}
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.currentTarget === e.target) {
|
||||
inputBoxRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* top row */}
|
||||
<>
|
||||
{/* selections */}
|
||||
{(selections && selections.length !== 0) &&
|
||||
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
|
||||
}
|
||||
|
||||
{/* error message */}
|
||||
{latestError === null ? null :
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
onDismiss={() => { setLatestError(null) }}
|
||||
showDismiss={true}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
{/* middle row */}
|
||||
<div
|
||||
className={
|
||||
// // hack to overwrite vscode styles (generated with this code):
|
||||
// `bg-transparent outline-none text-vscode-input-fg min-h-[81px] max-h-[500px]`
|
||||
// .split(' ')
|
||||
// .map(style => `@@[&_textarea]:!void-${style}`) // apply styles to ancestor textarea elements
|
||||
// .join(' ') +
|
||||
// ` outline-none border-none`
|
||||
// .split(' ')
|
||||
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`)
|
||||
// .join(' ');
|
||||
`@@[&_textarea]:!void-bg-transparent
|
||||
@@[&_textarea]:!void-outline-none
|
||||
@@[&_textarea]:!void-text-vscode-input-fg
|
||||
@@[&_textarea]:!void-min-h-[81px]
|
||||
@@[&_textarea]:!void-max-h-[500px]
|
||||
@@[&_div.monaco-inputbox]:!void-border-none
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none`
|
||||
}
|
||||
>
|
||||
|
||||
{/* text input */}
|
||||
<VoidInputBox
|
||||
placeholder={`${getCmdKey()}+L to select`}
|
||||
onChangeText={onChangeText}
|
||||
inputBoxRef={inputBoxRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* error message */}
|
||||
{latestError === null ? null :
|
||||
<ErrorDisplay
|
||||
error={latestError}
|
||||
onDismiss={() => { setLatestError(null) }}
|
||||
/>}
|
||||
</div>
|
||||
</>
|
||||
{/* bottom row */}
|
||||
<div
|
||||
className='flex flex-row justify-between items-end gap-1'
|
||||
>
|
||||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isLoading ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onAbort}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div >
|
||||
</div >
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useConfigState, useService } from '../util/services.js';
|
||||
import { IVoidConfigStateService, nonDefaultConfigFields, PartialVoidConfig, VoidConfig, VoidConfigField, VoidConfigInfo, SetFieldFnType, ConfigState } from '../../../registerConfig.js';
|
||||
|
||||
|
||||
const SettingOfFieldAndParam = ({ field, param, configState, configStateService }:
|
||||
{ field: VoidConfigField; param: string; configState: ConfigState; configStateService: IVoidConfigStateService }) => {
|
||||
|
||||
const { partialVoidConfig } = configState
|
||||
|
||||
|
||||
const { enumArr, defaultVal, description } = configStateService.voidConfigInfo[field][param]
|
||||
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
|
||||
|
||||
const updateState = (newValue: string) => { configStateService.setField(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 configState = useConfigState()
|
||||
const configStateService = useService('configStateService')
|
||||
|
||||
const { voidConfig } = configState
|
||||
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
|
||||
configState={configState}
|
||||
configStateService={configStateService}
|
||||
field='default'
|
||||
param='whichApi'
|
||||
/>
|
||||
<SettingOfFieldAndParam
|
||||
configState={configState}
|
||||
configStateService={configStateService}
|
||||
field='default'
|
||||
param='maxTokens'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* render all fields, but hide the ones not visible for fast tab switching */}
|
||||
{nonDefaultConfigFields.map(field => {
|
||||
return <div
|
||||
key={field}
|
||||
className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
|
||||
>
|
||||
{Object.keys(configStateService.voidConfigInfo[field]).map((param) => (
|
||||
<SettingOfFieldAndParam
|
||||
key={param}
|
||||
configState={configState}
|
||||
configStateService={configStateService}
|
||||
field={field}
|
||||
param={param}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+24
-14
@@ -1,9 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React from "react";
|
||||
import { useService, useThreadsState } from '../util/services.js';
|
||||
import { useAccessor, useThreadsState } from '../util/services.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
@@ -17,16 +20,18 @@ const truncate = (s: string) => {
|
||||
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useThreadsState()
|
||||
const threadsStateService = useService('threadsStateService')
|
||||
const sidebarStateService = useService('sidebarStateService')
|
||||
|
||||
const accessor = useAccessor()
|
||||
const threadsStateService = accessor.get('IThreadHistoryService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
|
||||
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">
|
||||
<div className="flex flex-col gap-y-1 max-h-[400px] overflow-y-auto">
|
||||
|
||||
{/* X button at top right */}
|
||||
<div className="text-right">
|
||||
@@ -48,7 +53,7 @@ export const SidebarThreadSelector = () => {
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
<div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
|
||||
<div className='flex flex-col gap-y-1 overflow-y-auto'>
|
||||
{sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads)
|
||||
return <>Error: Threads not found.</>
|
||||
@@ -56,21 +61,26 @@ export const SidebarThreadSelector = () => {
|
||||
|
||||
let btnStringArr: string[] = []
|
||||
|
||||
let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
|
||||
btnStringArr.push(msg1)
|
||||
const firstMsgIdx = allThreads[threadId].messages.findIndex(msg => msg.role !== 'system' && !!msg.displayContent) ?? ''
|
||||
if (firstMsgIdx !== -1)
|
||||
btnStringArr.push(truncate(allThreads[threadId].messages[firstMsgIdx].displayContent ?? ''))
|
||||
else
|
||||
btnStringArr.push('""')
|
||||
|
||||
let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
|
||||
if (msg2)
|
||||
btnStringArr.push(msg2)
|
||||
const secondMsgIdx = allThreads[threadId].messages.findIndex((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx) ?? ''
|
||||
if (secondMsgIdx !== -1)
|
||||
btnStringArr.push(truncate(allThreads[threadId].messages[secondMsgIdx].displayContent ?? ''))
|
||||
|
||||
btnStringArr.push(allThreads[threadId].messages.length + '')
|
||||
const numMessagesRemaining = allThreads[threadId].messages.filter((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > secondMsgIdx).length
|
||||
if (numMessagesRemaining > 0)
|
||||
btnStringArr.push(numMessagesRemaining + '')
|
||||
|
||||
const btnString = btnStringArr.join(' / ')
|
||||
|
||||
return (
|
||||
<button
|
||||
key={pastThread.id}
|
||||
className={`btn btn-sm rounded-sm ${pastThread.id === threadsStateService.getCurrentThread(threadsState)?.id ? "btn-primary" : "btn-secondary"}`}
|
||||
className={`rounded-sm`}
|
||||
onClick={() => threadsStateService.switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { Sidebar } from './Sidebar.js'
|
||||
|
||||
export const mountSidebar = mountFnGenerator(Sidebar)
|
||||
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
.select-child-restyle select {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
* {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* html {
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
|
||||
import { getCmdKey } from '../../../getCmdKey.js';
|
||||
|
||||
// const opaqueMessage = `\
|
||||
// Unfortunately, Void can't see the full error. However, you should be able to find more details by pressing ${getCmdKey()}+Shift+P, typing "Toggle Developer Tools", and looking at the console.\n
|
||||
// This error often means you have an incorrect API key. If you're self-hosting your own server, it might mean your CORS headers are off, and you should make sure your server's response has the header "Access-Control-Allow-Origins" set to "*", or at least allows "vscode-file://vscode-app".`
|
||||
// if ((error instanceof Error) && (error.cause + '').includes('TypeError: Failed to fetch')) {
|
||||
// e = error as any
|
||||
// e['Void Team'] = opaqueMessage
|
||||
// }
|
||||
|
||||
|
||||
type Details = {
|
||||
message: string,
|
||||
name: string,
|
||||
stack: string | null,
|
||||
cause: string | null,
|
||||
code: string | null,
|
||||
additional: Record<string, any>
|
||||
}
|
||||
|
||||
// Get detailed error information
|
||||
const getErrorDetails = (error: unknown) => {
|
||||
|
||||
let details: Details;
|
||||
|
||||
let e: Error & { [other: string]: undefined | any }
|
||||
|
||||
// If fetch() fails, it gives an opaque message. We add extra details to the error.
|
||||
if (error instanceof Error) {
|
||||
e = error
|
||||
}
|
||||
// sometimes error is an object but not an Error
|
||||
else if (typeof error === 'object') {
|
||||
e = new Error('More details below.', { cause: JSON.stringify(error) })
|
||||
}
|
||||
else {
|
||||
e = new Error(String(error))
|
||||
}
|
||||
// console.log('error display', JSON.stringify(e))
|
||||
|
||||
const message = e.message && e.error ?
|
||||
(e.message + ':\n' + e.error)
|
||||
: e.message || e.error || JSON.stringify(error)
|
||||
|
||||
details = {
|
||||
name: e.name || 'Error',
|
||||
message: message,
|
||||
stack: null, // e.stack is ignored because it's ugly and not very useful
|
||||
cause: e.cause ? String(e.cause) : null,
|
||||
code: e.code || null,
|
||||
additional: {}
|
||||
}
|
||||
|
||||
|
||||
// Collect any additional properties from the e
|
||||
for (let prop of Object.getOwnPropertyNames(e).filter((prop) => !Object.keys(details).includes(prop)))
|
||||
details.additional[prop] = (e as any)[prop]
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
error,
|
||||
onDismiss = null,
|
||||
showDismiss = true,
|
||||
className = ''
|
||||
}: {
|
||||
error: Error | object | string,
|
||||
onDismiss: (() => void) | null,
|
||||
showDismiss?: boolean,
|
||||
className?: string
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const details = getErrorDetails(error);
|
||||
const hasDetails = details.cause || Object.keys(details.additional).length > 0;
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-red-500 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-red-800">
|
||||
{details.name}
|
||||
</h3>
|
||||
<p className="text-red-700 mt-1">
|
||||
{details.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{hasDetails && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-5 w-5" />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{showDismiss && onDismiss && (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && hasDetails && (
|
||||
<div className="mt-4 space-y-3 border-t border-red-200 pt-3">
|
||||
{details.code && (
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Error Code: </span>
|
||||
<span className="text-red-700">{details.code}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{details.cause && (
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Cause: </span>
|
||||
<span className="text-red-700">{details.cause}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(details.additional).length > 0 && (
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Additional Information:</span>
|
||||
<pre className="mt-1 text-sm text-red-700 overflow-x-auto whitespace-pre-wrap">
|
||||
{Object.keys(details.additional).map(key => `${key}:\n${details.additional[key]}`).join('\n')}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{/* {details.stack && (
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Stack Trace:</span>
|
||||
<pre className="mt-1 text-sm text-red-700 overflow-x-auto whitespace-pre-wrap">
|
||||
{details.stack}
|
||||
</pre>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
export { diffLines, Change }
|
||||
@@ -0,0 +1,508 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js';
|
||||
|
||||
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
|
||||
import { useAccessor } from './services.js';
|
||||
|
||||
|
||||
// type guard
|
||||
const isConstructor = (f: any)
|
||||
: f is { new(...params: any[]): any } => {
|
||||
return !!f.prototype && f.prototype.constructor === f;
|
||||
}
|
||||
|
||||
export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, propsFn, dispose, onCreateInstance, children, className }
|
||||
: {
|
||||
ctor: { new(...params: CtorParams): Instance } | ((container: HTMLDivElement) => Instance),
|
||||
propsFn: (container: HTMLDivElement) => CtorParams, // unused if fn
|
||||
onCreateInstance: (instance: Instance) => IDisposable[],
|
||||
dispose: (instance: Instance) => void,
|
||||
children?: React.ReactNode,
|
||||
className?: string
|
||||
}
|
||||
) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const instance = isConstructor(ctor) ? new ctor(...propsFn(containerRef.current!)) : ctor(containerRef.current!)
|
||||
const disposables = onCreateInstance(instance);
|
||||
return () => {
|
||||
disposables.forEach(d => d.dispose());
|
||||
dispose(instance)
|
||||
}
|
||||
}, [ctor, propsFn, dispose, onCreateInstance, containerRef])
|
||||
|
||||
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline, styles }: {
|
||||
onChangeText: (value: string) => void;
|
||||
styles?: Partial<IInputBoxStyles>,
|
||||
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
|
||||
inputBoxRef?: { current: InputBox | null };
|
||||
placeholder: string;
|
||||
multiline: boolean;
|
||||
}) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
return <WidgetComponent
|
||||
ctor={InputBox}
|
||||
propsFn={useCallback((container) => [
|
||||
container,
|
||||
contextViewProvider,
|
||||
{
|
||||
inputBoxStyles: {
|
||||
...defaultInputBoxStyles,
|
||||
// inputBackground: 'transparent',
|
||||
// inputBorder: 'none',
|
||||
...styles,
|
||||
},
|
||||
placeholder,
|
||||
tooltip: '',
|
||||
flexibleHeight: multiline,
|
||||
flexibleMaxHeight: 500,
|
||||
flexibleWidth: true,
|
||||
}
|
||||
] as const, [contextViewProvider, placeholder, multiline])}
|
||||
dispose={useCallback((instance: InputBox) => {
|
||||
instance.dispose()
|
||||
instance.element.remove()
|
||||
}, [])}
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
instance.onDidChange((newText) => onChangeText(newText))
|
||||
)
|
||||
if (onCreateInstance) {
|
||||
const ds = onCreateInstance(instance) ?? []
|
||||
disposables.push(...ds)
|
||||
}
|
||||
if (inputBoxRef)
|
||||
inputBoxRef.current = instance;
|
||||
|
||||
return disposables
|
||||
}, [onChangeText, onCreateInstance, inputBoxRef])
|
||||
}
|
||||
/>
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const VoidSwitch = ({
|
||||
value,
|
||||
onChange,
|
||||
size = 'md',
|
||||
label,
|
||||
disabled = false,
|
||||
}: {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
size?: 'xs' | 'sm' | 'sm+' | 'md';
|
||||
}) => {
|
||||
return (
|
||||
<label className="inline-flex items-center cursor-pointer">
|
||||
<div
|
||||
onClick={() => !disabled && onChange(!value)}
|
||||
className={`
|
||||
relative inline-flex items-center rounded-full transition-colors duration-200 ease-in-out
|
||||
${value ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}
|
||||
${disabled ? 'opacity-25' : ''}
|
||||
${size === 'xs' ? 'h-4 w-7' : ''}
|
||||
${size === 'sm' ? 'h-5 w-9' : ''}
|
||||
${size === 'sm+' ? 'h-5 w-10' : ''}
|
||||
${size === 'md' ? 'h-6 w-11' : ''}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block transform rounded-full bg-white dark:bg-gray-900 shadow transition-transform duration-200 ease-in-out
|
||||
${size === 'xs' ? 'h-2.5 w-2.5' : ''}
|
||||
${size === 'sm' ? 'h-3 w-3' : ''}
|
||||
${size === 'sm+' ? 'h-3.5 w-3.5' : ''}
|
||||
${size === 'md' ? 'h-4 w-4' : ''}
|
||||
${size === 'xs' ? (value ? 'translate-x-3.5' : 'translate-x-0.5') : ''}
|
||||
${size === 'sm' ? (value ? 'translate-x-5' : 'translate-x-1') : ''}
|
||||
${size === 'sm+' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
|
||||
${size === 'md' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
{label && (
|
||||
<span className={`
|
||||
ml-3 font-medium text-gray-900 dark:text-gray-100
|
||||
${size === 'xs' ? 'text-xs' : 'text-sm'}
|
||||
`}>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const VoidCheckBox = ({ label, value, onClick, className }: { label: string, value: boolean, onClick: (checked: boolean) => void, className?: string }) => {
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
const instanceRef = useRef<Checkbox | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!instanceRef.current) return
|
||||
instanceRef.current.checked = value
|
||||
}, [value])
|
||||
|
||||
|
||||
return <WidgetComponent
|
||||
className={className ?? ''}
|
||||
ctor={Checkbox}
|
||||
propsFn={useCallback((container: HTMLDivElement) => {
|
||||
divRef.current = container
|
||||
return [label, value, defaultCheckboxStyles] as const
|
||||
}, [label, value])}
|
||||
onCreateInstance={useCallback((instance: Checkbox) => {
|
||||
instanceRef.current = instance;
|
||||
divRef.current?.append(instance.domNode)
|
||||
const d = instance.onChange(() => onClick(instance.checked))
|
||||
return [d]
|
||||
}, [onClick])}
|
||||
dispose={useCallback((instance: Checkbox) => {
|
||||
instance.dispose()
|
||||
instance.domNode.remove()
|
||||
}, [])}
|
||||
|
||||
/>
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options }: {
|
||||
onChangeSelection: (value: T) => void;
|
||||
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
|
||||
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
|
||||
options: readonly { text: string, value: T }[];
|
||||
}) => {
|
||||
const accessor = useAccessor()
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
|
||||
let containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return <WidgetComponent
|
||||
className='@@select-child-restyle'
|
||||
ctor={SelectBox}
|
||||
propsFn={useCallback((container) => {
|
||||
containerRef.current = container
|
||||
const defaultIndex = 0;
|
||||
return [
|
||||
options.map(opt => ({ text: opt.text })),
|
||||
defaultIndex,
|
||||
contextViewProvider,
|
||||
defaultSelectBoxStyles
|
||||
] as const;
|
||||
}, [containerRef, options, contextViewProvider])}
|
||||
|
||||
dispose={useCallback((instance: SelectBox) => {
|
||||
instance.dispose();
|
||||
for (let child of containerRef.current?.childNodes ?? [])
|
||||
containerRef.current?.removeChild(child)
|
||||
}, [containerRef])}
|
||||
|
||||
onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
const disposables: IDisposable[] = []
|
||||
|
||||
if (containerRef.current)
|
||||
instance.render(containerRef.current)
|
||||
|
||||
disposables.push(
|
||||
instance.onDidSelect(e => { onChangeSelection(options[e.index].value); })
|
||||
)
|
||||
|
||||
if (onCreateInstance) {
|
||||
const ds = onCreateInstance(instance) ?? []
|
||||
disposables.push(...ds)
|
||||
}
|
||||
if (selectBoxRef)
|
||||
selectBoxRef.current = instance;
|
||||
|
||||
return disposables;
|
||||
}, [containerRef, onChangeSelection, options, onCreateInstance, selectBoxRef])}
|
||||
|
||||
/>;
|
||||
};
|
||||
|
||||
// makes it so that code in the sidebar isnt too tabbed out
|
||||
const normalizeIndentation = (code: string): string => {
|
||||
const lines = code.split('\n')
|
||||
|
||||
let minLeadingSpaces = Infinity
|
||||
|
||||
// find the minimum number of leading spaces
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') continue;
|
||||
let leadingSpaces = 0;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
if (char === '\t' || char === ' ') {
|
||||
leadingSpaces += 1;
|
||||
} else { break; }
|
||||
}
|
||||
minLeadingSpaces = Math.min(minLeadingSpaces, leadingSpaces)
|
||||
}
|
||||
|
||||
// remove the leading spaces
|
||||
return lines.map(line => {
|
||||
if (line.trim() === '') return line;
|
||||
|
||||
let spacesToRemove = minLeadingSpaces;
|
||||
let i = 0;
|
||||
while (spacesToRemove > 0 && i < line.length) {
|
||||
const char = line[i];
|
||||
if (char === '\t' || char === ' ') {
|
||||
spacesToRemove -= 1;
|
||||
i++;
|
||||
} else { break; }
|
||||
}
|
||||
|
||||
return line.slice(i);
|
||||
|
||||
}).join('\n')
|
||||
|
||||
}
|
||||
|
||||
export const VoidCodeEditor = ({ initValue, language }: { initValue: string, language: string | undefined }) => {
|
||||
|
||||
const MAX_HEIGHT = Infinity;
|
||||
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const instantiationService = accessor.get('IInstantiationService')
|
||||
const modelService = accessor.get('IModelService')
|
||||
|
||||
initValue = normalizeIndentation(initValue)
|
||||
|
||||
return <div ref={divRef}>
|
||||
<WidgetComponent
|
||||
className='relative z-0 text-sm'
|
||||
ctor={useCallback((container) =>
|
||||
instantiationService.createInstance(
|
||||
CodeEditorWidget,
|
||||
container,
|
||||
{
|
||||
automaticLayout: true,
|
||||
wordWrap: 'off',
|
||||
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
|
||||
lineNumbers: 'off',
|
||||
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
readOnlyMessage: { value: '' },
|
||||
|
||||
minimap: {
|
||||
enabled: false,
|
||||
// maxColumn: 0,
|
||||
},
|
||||
|
||||
selectionHighlight: false, // highlights whole words
|
||||
renderLineHighlight: 'none',
|
||||
|
||||
folding: false,
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
glyphMargin: false,
|
||||
|
||||
stickyScroll: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
isSimpleWidget: true,
|
||||
})
|
||||
, [instantiationService])
|
||||
}
|
||||
|
||||
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
|
||||
const model = modelService.createModel(initValue, null)
|
||||
editor.setModel(model);
|
||||
model.setLanguage(language ?? 'plaintext')
|
||||
|
||||
const container = editor.getDomNode()
|
||||
const parentNode = container?.parentElement
|
||||
if (parentNode) {
|
||||
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
|
||||
parentNode.style.height = `${height}px`;
|
||||
editor.layout();
|
||||
}
|
||||
|
||||
// Listen for content changes and update height
|
||||
const disposable = editor.onDidContentSizeChange(() => {
|
||||
if (parentNode) {
|
||||
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
|
||||
parentNode.style.height = `${height}px`;
|
||||
editor.layout();
|
||||
}
|
||||
});
|
||||
|
||||
return [disposable]
|
||||
}, [modelService, initValue, language])}
|
||||
|
||||
dispose={useCallback((editor: CodeEditorWidget) => {
|
||||
editor.dispose();
|
||||
}, [])}
|
||||
|
||||
propsFn={useCallback(() => { return [] }, [])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
|
||||
// const instanceRef = useRef<DomScrollableElement | null>(null);
|
||||
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)
|
||||
|
||||
// return <>
|
||||
// <WidgetComponent
|
||||
// ctor={DomScrollableElement}
|
||||
// propsFn={useCallback((container) => {
|
||||
// return [container, options] as const;
|
||||
// }, [options])}
|
||||
// onCreateInstance={useCallback((instance: DomScrollableElement) => {
|
||||
// instanceRef.current = instance;
|
||||
// setChildrenPortal(createPortal(children, instance.getDomNode()))
|
||||
// return []
|
||||
// }, [setChildrenPortal, children])}
|
||||
// dispose={useCallback((instance: DomScrollableElement) => {
|
||||
// console.log('calling dispose!!!!')
|
||||
// // instance.dispose();
|
||||
// // instance.getDomNode().remove()
|
||||
// }, [])}
|
||||
// >{children}</WidgetComponent>
|
||||
|
||||
// {childrenPortal}
|
||||
|
||||
// </>
|
||||
// }
|
||||
|
||||
// export const VoidSelectBox = <T,>({ onChangeSelection, initVal, selectBoxRef, options }: {
|
||||
// initVal: T;
|
||||
// selectBoxRef: React.MutableRefObject<SelectBox | null>;
|
||||
// options: readonly { text: string, value: T }[];
|
||||
// onChangeSelection: (value: T) => void;
|
||||
// }) => {
|
||||
|
||||
|
||||
// return <WidgetComponent
|
||||
// ctor={DropdownMenu}
|
||||
// propsFn={useCallback((container) => {
|
||||
// return [
|
||||
// container, {
|
||||
// contextMenuProvider,
|
||||
// actions: options.map(({ text, value }, i) => ({
|
||||
// id: i + '',
|
||||
// label: text,
|
||||
// tooltip: text,
|
||||
// class: undefined,
|
||||
// enabled: true,
|
||||
// run: () => {
|
||||
// onChangeSelection(value);
|
||||
// },
|
||||
// }))
|
||||
|
||||
// }] as const;
|
||||
// }, [options, initVal, contextViewProvider])}
|
||||
|
||||
// dispose={useCallback((instance: DropdownMenu) => {
|
||||
// instance.dispose();
|
||||
// // instance.element.remove()
|
||||
// }, [])}
|
||||
|
||||
// onCreateInstance={useCallback((instance: DropdownMenu) => {
|
||||
// return []
|
||||
// }, [])}
|
||||
|
||||
// />;
|
||||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
// export const VoidCheckBox = ({ onChangeChecked, initVal, label, checkboxRef, }: {
|
||||
// onChangeChecked: (checked: boolean) => void;
|
||||
// initVal: boolean;
|
||||
// checkboxRef: React.MutableRefObject<ObjectSettingCheckboxWidget | null>;
|
||||
// label: string;
|
||||
// }) => {
|
||||
// const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!containerRef.current) return;
|
||||
|
||||
// // Create and mount the Checkbox using VSCode's implementation
|
||||
|
||||
// checkboxRef.current = new ObjectSettingCheckboxWidget(
|
||||
// containerRef.current,
|
||||
// themeService,
|
||||
// contextViewService,
|
||||
// hoverService,
|
||||
// );
|
||||
|
||||
|
||||
// checkboxRef.current.setValue([{
|
||||
// key: { type: 'string', data: label },
|
||||
// value: { type: 'boolean', data: initVal },
|
||||
// removable: false,
|
||||
// resetable: true,
|
||||
// }])
|
||||
|
||||
// checkboxRef.current.onDidChangeList((list) => {
|
||||
// onChangeChecked(!!list);
|
||||
// })
|
||||
|
||||
|
||||
// // cleanup
|
||||
// return () => {
|
||||
// if (checkboxRef.current) {
|
||||
// checkboxRef.current.dispose();
|
||||
// if (containerRef.current) {
|
||||
// while (containerRef.current.firstChild) {
|
||||
// containerRef.current.removeChild(containerRef.current.firstChild);
|
||||
// }
|
||||
// }
|
||||
// checkboxRef.current = null;
|
||||
// }
|
||||
// };
|
||||
// }, [checkboxRef, label, initVal, onChangeChecked]);
|
||||
|
||||
// return <div ref={containerRef} className="w-full" />;
|
||||
// };
|
||||
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as ReactDOM from 'react-dom/client'
|
||||
import { ReactServicesType, VoidSidebarState } from '../../../registerSidebar.js';
|
||||
import { _registerServices } from './services.js';
|
||||
|
||||
|
||||
export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLElement, services: ReactServicesType) => {
|
||||
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
|
||||
|
||||
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, accessor: ServicesAccessor, props?: any) => {
|
||||
if (typeof document === 'undefined') {
|
||||
console.error('index.tsx error: document was undefined')
|
||||
return
|
||||
}
|
||||
|
||||
_registerServices(services)
|
||||
const disposables = _registerServices(accessor)
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(<Component />);
|
||||
root.render(<Component {...props} />); // tailwind dark theme indicator
|
||||
|
||||
return disposables
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
export { posthog }
|
||||
@@ -1,986 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { Ollama } from 'ollama/browser'
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
import { posthog } from 'posthog-js'
|
||||
import type { VoidConfig } from '../../../registerConfig.js';
|
||||
import type { LLMMessage, OnText, OnError, OnFinalMessage, SendLLMMMessageParams, } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
|
||||
type SendLLMMessageFnTypeInternal = (params: {
|
||||
messages: LLMMessage[];
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
voidConfig: VoidConfig;
|
||||
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}) => void
|
||||
|
||||
|
||||
const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
if (Number.isNaN(int))
|
||||
return undefined
|
||||
return int
|
||||
}
|
||||
|
||||
// Anthropic
|
||||
type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// remove system messages for Anthropic
|
||||
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: systemMessage,
|
||||
messages: anthropicMessages,
|
||||
model: voidConfig.anthropic.model,
|
||||
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
|
||||
});
|
||||
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText })
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
// stringify the response's content
|
||||
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
|
||||
onFinalMessage({ fullText: 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({ error: 'Invalid API key.' })
|
||||
}
|
||||
else {
|
||||
onError({ error })
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
|
||||
// Gemini
|
||||
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
|
||||
const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
|
||||
|
||||
// remove system messages that get sent to Gemini
|
||||
// str of all system messages
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// Convert messages to Gemini format
|
||||
const geminiMessages: Content[] = messages
|
||||
.filter(msg => msg.role !== 'system')
|
||||
.map((msg, i) => ({
|
||||
parts: [{ text: msg.content }],
|
||||
role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
}))
|
||||
|
||||
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
|
||||
.then(async response => {
|
||||
_setAborter(() => response.stream.return(fullText))
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
const newText = chunk.text();
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
|
||||
onError({ error: 'Invalid API key.' });
|
||||
}
|
||||
else {
|
||||
onError({ error });
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
|
||||
|
||||
if (voidConfig.default.whichApi === 'openAI') {
|
||||
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
|
||||
options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
}
|
||||
else if (voidConfig.default.whichApi === 'openRouter') {
|
||||
openai = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
});
|
||||
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
}
|
||||
else if (voidConfig.default.whichApi === 'openAICompatible') {
|
||||
openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
|
||||
options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
|
||||
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) {
|
||||
onError({ error: 'Invalid API key.' });
|
||||
}
|
||||
else {
|
||||
onError({ error });
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: voidConfig.ollama.model,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch(error => {
|
||||
onError({ error })
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
// Greptile
|
||||
// https://docs.greptile.com/api-reference/query
|
||||
// https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
fetch('https://api.greptile.com/v2/query', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${voidConfig.greptile.apikey}`,
|
||||
'X-Github-Token': `${voidConfig.greptile.githubPAT}`,
|
||||
'Content-Type': `application/json`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages,
|
||||
stream: true,
|
||||
repositories: [voidConfig.greptile.repoinfo],
|
||||
}),
|
||||
})
|
||||
// this is {message}\n{message}\n{message}...\n
|
||||
.then(async response => {
|
||||
const text = await response.text()
|
||||
console.log('got greptile', text)
|
||||
return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
})
|
||||
// TODO make this actually stream, right now it just sends one message at the end
|
||||
// TODO add _setAborter() when add streaming
|
||||
.then(async responseArr => {
|
||||
|
||||
for (const response of responseArr) {
|
||||
const type: string = response['type']
|
||||
const message = response['message']
|
||||
|
||||
// when receive text
|
||||
if (type === 'message') {
|
||||
fullText += message
|
||||
onText({ newText: message, fullText })
|
||||
}
|
||||
else if (type === 'sources') {
|
||||
const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
|
||||
fullText += filepath
|
||||
onText({ newText: filepath, fullText })
|
||||
}
|
||||
// type: 'status' with an empty 'message' means last message
|
||||
else if (type === 'status') {
|
||||
if (!message) {
|
||||
onFinalMessage({ fullText })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ error })
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
messages,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
voidConfig,
|
||||
logging: { loggingName }
|
||||
}: SendLLMMMessageParams) => {
|
||||
if (!voidConfig) return;
|
||||
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
posthog.capture(eventId, {
|
||||
whichApi: voidConfig.default['whichApi'],
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
version: '2024-11-14',
|
||||
...extras,
|
||||
})
|
||||
}
|
||||
const submit_time = new Date()
|
||||
|
||||
let _fullTextSoFar = ''
|
||||
let _aborter: (() => void) | null = null
|
||||
let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
let _didAbort = false
|
||||
|
||||
const onText: OnText = ({ newText, fullText }) => {
|
||||
if (_didAbort) return
|
||||
onText_({ newText, fullText })
|
||||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_({ fullText })
|
||||
}
|
||||
|
||||
const onError: OnError = ({ error }) => {
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
_aborter?.()
|
||||
_didAbort = true
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
|
||||
|
||||
try {
|
||||
switch (voidConfig.default.whichApi) {
|
||||
case 'anthropic':
|
||||
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'ollama':
|
||||
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'greptile':
|
||||
sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
default:
|
||||
onError({ error: `Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!` })
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
if (error instanceof Error) { onError({ error }) }
|
||||
else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); }
|
||||
; (_aborter as any)?.()
|
||||
_didAbort = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // 6. Autocomplete
|
||||
// const autocompleteProvider = new AutocompleteProvider(context);
|
||||
// context.subscriptions.push(vscode.languages.registerInlineCompletionItemProvider('*', autocompleteProvider));
|
||||
|
||||
// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
// // setupAutocomplete({ voidConfig, abortRef })
|
||||
|
||||
// // 7. Language Server
|
||||
// console.log('run lsp')
|
||||
// let disposable = vscode.commands.registerCommand('typeInspector.inspect', runTreeSitter);
|
||||
// context.subscriptions.push(disposable);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// import { configFields, VoidConfig } from "../webviews/common/contextForConfig"
|
||||
// import { FimInfo } from "./sendLLMMessage"
|
||||
|
||||
|
||||
// type GetFIMPrompt = ({ voidConfig, fimInfo }: { voidConfig: VoidConfig, fimInfo: FimInfo, }) => string
|
||||
|
||||
// export const getFIMSystem: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
|
||||
|
||||
// switch (voidConfig.default.whichApi) {
|
||||
// case 'ollama':
|
||||
// return ''
|
||||
// case 'anthropic':
|
||||
// case 'openAI':
|
||||
// case 'gemini':
|
||||
// case 'greptile':
|
||||
// case 'openRouter':
|
||||
// case 'openAICompatible':
|
||||
// case 'azure':
|
||||
// default:
|
||||
// return `You are given the START and END to a piece of code. Please FILL IN THE MIDDLE between the START and END.
|
||||
|
||||
// Instruction summary:
|
||||
// 1. Return the MIDDLE of the code between the START and END.
|
||||
// 2. Do not give an explanation, description, or any other code besides the middle.
|
||||
// 3. Do not return duplicate code from either START or END.
|
||||
// 4. Make sure the MIDDLE piece of code has balanced brackets that match the START and END.
|
||||
// 5. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line.
|
||||
// 6. Around 90% of the time, you should return just one or a few lines of code. You should keep your outputs short unless you are confident the user is trying to write boilderplate code.
|
||||
|
||||
// # EXAMPLE
|
||||
|
||||
// ## START:
|
||||
// \`\`\` python
|
||||
// def add(a,b):
|
||||
// return a + b
|
||||
// def subtract(a,b):
|
||||
// return a - b
|
||||
// \`\`\`
|
||||
// ## END:
|
||||
// \`\`\` python
|
||||
// def divide(a,b):
|
||||
// return a / b
|
||||
// \`\`\`
|
||||
// ## EXPECTED OUTPUT:
|
||||
// \`\`\` python
|
||||
|
||||
// def multiply(a,b):
|
||||
// return a * b
|
||||
// \`\`\`
|
||||
|
||||
// # EXAMPLE
|
||||
// ## START:
|
||||
// \`\`\` javascript
|
||||
// const x = 1
|
||||
|
||||
// const y
|
||||
// \`\`\`
|
||||
// ## END:
|
||||
// \`\`\` javascript
|
||||
|
||||
// const z = 3
|
||||
// \`\`\`
|
||||
// ## EXPECTED OUTPUT:
|
||||
// \`\`\` javascript
|
||||
// = 2
|
||||
// \`\`\`
|
||||
// `
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
|
||||
|
||||
// const { prefix: fullPrefix, suffix: fullSuffix } = fimInfo
|
||||
// const prefix = fullPrefix.split('\n').slice(-20).join('\n')
|
||||
// const suffix = fullSuffix.split('\n').slice(0, 20).join('\n')
|
||||
|
||||
|
||||
// console.log('prefix', JSON.stringify(prefix))
|
||||
// console.log('suffix', JSON.stringify(suffix))
|
||||
|
||||
// if (!prefix.trim() && !suffix.trim()) return ''
|
||||
|
||||
// // TODO may want to trim the prefix and suffix
|
||||
// switch (voidConfig.default.whichApi) {
|
||||
// case 'ollama':
|
||||
// if (voidConfig.ollama.model === 'codestral') {
|
||||
// return `[SUFFIX]${suffix}[PREFIX] ${prefix}`
|
||||
// } else if (voidConfig.ollama.model.includes('qwen')) {
|
||||
// return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`
|
||||
// }
|
||||
// return ''
|
||||
// case 'anthropic':
|
||||
// case 'openAI':
|
||||
// case 'gemini':
|
||||
// case 'greptile':
|
||||
// case 'openRouter':
|
||||
// case 'openAICompatible':
|
||||
// case 'azure':
|
||||
// default:
|
||||
// return `## START:
|
||||
// \`\`\`
|
||||
// ${prefix}
|
||||
// \`\`\`
|
||||
// ## END:
|
||||
// \`\`\`
|
||||
// ${suffix}
|
||||
// \`\`\`
|
||||
// `
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Mathew - sendLLMMessage
|
||||
|
||||
// import Anthropic from '@anthropic-ai/sdk';
|
||||
// import OpenAI from 'openai';
|
||||
// import { Ollama } from 'ollama/browser'
|
||||
// import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
// import { VoidConfig } from '../webviews/common/contextForConfig'
|
||||
// import { getFIMPrompt, getFIMSystem } from './getPrompt';
|
||||
|
||||
// export type AbortRef = { current: (() => void) }
|
||||
|
||||
// export type LLMMessageOnText = (newText: string, fullText: string) => void
|
||||
|
||||
// export type OnFinalMessage = (input: string) => void
|
||||
|
||||
// export type LLMMessageAnthropic = {
|
||||
// role: 'user' | 'assistant',
|
||||
// content: string,
|
||||
// }
|
||||
|
||||
// export type LLMMessage = {
|
||||
// role: 'system' | 'user' | 'assistant',
|
||||
// content: string,
|
||||
// }
|
||||
|
||||
// type LLMMessageOptions = { stopTokens?: string[] }
|
||||
|
||||
// type SendLLMMessageFnTypeInternal = (params: {
|
||||
// mode: 'chat' | 'fim',
|
||||
// messages: LLMMessage[],
|
||||
// options?: LLMMessageOptions,
|
||||
// onText: LLMMessageOnText,
|
||||
// onFinalMessage: OnFinalMessage,
|
||||
// onError: (error: string) => void,
|
||||
// abortRef: AbortRef,
|
||||
// voidConfig: VoidConfig,
|
||||
// }) => void
|
||||
|
||||
|
||||
// type SendLLMMessageFnTypeExternal = (params: (
|
||||
// | { mode?: 'chat', messages: LLMMessage[], fimInfo?: undefined, }
|
||||
// | { mode: 'fim', messages?: undefined, fimInfo: FimInfo, }
|
||||
// ) & {
|
||||
// options?: LLMMessageOptions,
|
||||
// onText: LLMMessageOnText,
|
||||
// onFinalMessage: OnFinalMessage,
|
||||
// onError: (error: string) => void,
|
||||
// abortRef: AbortRef,
|
||||
// voidConfig: VoidConfig | null, // these may be absent
|
||||
// }) => void
|
||||
|
||||
// export type FimInfo = {
|
||||
// prefix: string,
|
||||
// suffix: string,
|
||||
// }
|
||||
|
||||
// const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
// // Anthropic
|
||||
// const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
|
||||
// const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
|
||||
|
||||
// // find system messages and concatenate them
|
||||
// const systemMessage = messages
|
||||
// .filter(msg => msg.role === 'system')
|
||||
// .map(msg => msg.content)
|
||||
// .join('\n');
|
||||
|
||||
// // remove system messages for Anthropic
|
||||
// const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
// const stream = anthropic.messages.stream({
|
||||
// system: systemMessage,
|
||||
// messages: anthropicMessages,
|
||||
// model: voidConfig.anthropic.model,
|
||||
// max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
|
||||
// });
|
||||
|
||||
// let did_abort = false
|
||||
|
||||
// // when receive text
|
||||
// stream.on('text', (newText, fullText) => {
|
||||
// if (did_abort) return
|
||||
// onText(newText, fullText)
|
||||
// })
|
||||
|
||||
// // when we get the final message on this stream (or when error/fail)
|
||||
// stream.on('finalMessage', (claude_response) => {
|
||||
// if (did_abort) return
|
||||
// // stringify the response's content
|
||||
// let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n');
|
||||
// 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 = () => {
|
||||
// did_abort = true
|
||||
// stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
|
||||
// }
|
||||
|
||||
// return { abort }
|
||||
// };
|
||||
|
||||
// // Gemini
|
||||
// const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
// let didAbort = false
|
||||
// let fullText = ''
|
||||
|
||||
// abortRef.current = () => {
|
||||
// didAbort = true
|
||||
// }
|
||||
|
||||
// const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
|
||||
// const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
|
||||
|
||||
// // remove system messages that get sent to Gemini
|
||||
// // str of all system messages
|
||||
// let systemMessage = messages
|
||||
// .filter(msg => msg.role === 'system')
|
||||
// .map(msg => msg.content)
|
||||
// .join('\n');
|
||||
|
||||
// // Convert messages to Gemini format
|
||||
// const geminiMessages: Content[] = messages
|
||||
// .filter(msg => msg.role !== 'system')
|
||||
// .map((msg, i) => ({
|
||||
// parts: [{ text: msg.content }],
|
||||
// role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
// }))
|
||||
|
||||
// model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
|
||||
// .then(async response => {
|
||||
// abortRef.current = () => {
|
||||
// // response.stream.return(fullText)
|
||||
// didAbort = true;
|
||||
// }
|
||||
// for await (const chunk of response.stream) {
|
||||
// if (didAbort) return;
|
||||
// const newText = chunk.text();
|
||||
// fullText += newText;
|
||||
// onText(newText, fullText);
|
||||
// }
|
||||
// onFinalMessage(fullText);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// if (error instanceof GoogleGenerativeAIFetchError) {
|
||||
// if (error.status === 400) {
|
||||
// onError('Invalid API key.');
|
||||
// }
|
||||
// else {
|
||||
// onError(`${error.name}:\n${error.message}`);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// onError(error);
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// // OpenAI, OpenRouter, OpenAICompatible
|
||||
// const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
// let didAbort = false
|
||||
// let fullText = ''
|
||||
|
||||
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
// abortRef.current = () => {
|
||||
// didAbort = true;
|
||||
// };
|
||||
|
||||
// let openai: OpenAI
|
||||
// let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
// let maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
|
||||
|
||||
// if (voidConfig.default.whichApi === 'openAI') {
|
||||
// openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
|
||||
// options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
// }
|
||||
// else if (voidConfig.default.whichApi === 'openRouter') {
|
||||
// openai = new OpenAI({
|
||||
// baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
|
||||
// defaultHeaders: {
|
||||
// "HTTP-Referer": 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
// "X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
// },
|
||||
// });
|
||||
// options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
// }
|
||||
// else if (voidConfig.default.whichApi === 'openAICompatible') {
|
||||
// openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
|
||||
// options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
// }
|
||||
// else {
|
||||
// console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
|
||||
// throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
|
||||
// }
|
||||
|
||||
// openai.chat.completions
|
||||
// .create(options)
|
||||
// .then(async response => {
|
||||
// abortRef.current = () => {
|
||||
// // response.controller.abort()
|
||||
// didAbort = true;
|
||||
// }
|
||||
// // when receive text
|
||||
// for await (const chunk of response) {
|
||||
// if (didAbort) return;
|
||||
// const newText = chunk.choices[0]?.delta?.content || '';
|
||||
// fullText += newText;
|
||||
// onText(newText, fullText);
|
||||
// }
|
||||
// onFinalMessage(fullText);
|
||||
// })
|
||||
// // when error/fail - this catches errors of both .create() and .then(for await)
|
||||
// .catch(error => {
|
||||
// if (error instanceof OpenAI.APIError) {
|
||||
// if (error.status === 401) {
|
||||
// onError('Invalid API key.');
|
||||
// }
|
||||
// else {
|
||||
// onError(`${error.name}:\n${error.message}`);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// onError(error);
|
||||
// }
|
||||
// })
|
||||
|
||||
// };
|
||||
|
||||
// // Ollama
|
||||
// export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
// let didAbort = false
|
||||
// let fullText = ""
|
||||
|
||||
// const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
// abortRef.current = () => {
|
||||
// didAbort = true;
|
||||
// };
|
||||
|
||||
// type GenerateResponse = Awaited<ReturnType<(typeof ollama.generate)>>
|
||||
// type ChatResponse = Awaited<ReturnType<(typeof ollama.chat)>>
|
||||
|
||||
|
||||
// // First check if model exists
|
||||
// ollama.list()
|
||||
// .then(async models => {
|
||||
// const installedModels = models.models.map(m => m.name.replace(/:latest$/, ''))
|
||||
// const modelExists = installedModels.some(m => m.startsWith(voidConfig.ollama.model));
|
||||
// if (!modelExists) {
|
||||
// const errorMessage = `The model "${voidConfig.ollama.model}" is not available locally. Please run 'ollama pull ${voidConfig.ollama.model}' to download it first or
|
||||
// try selecting one from the Installed models: ${installedModels.join(', ')}`;
|
||||
// onText(errorMessage, errorMessage);
|
||||
// onFinalMessage(errorMessage);
|
||||
// return Promise.reject();
|
||||
// }
|
||||
|
||||
// if (mode === 'fim') {
|
||||
// // the fim prompt is the last message
|
||||
// let prompt = messages[messages.length - 1].content
|
||||
// return ollama.generate({
|
||||
// model: voidConfig.ollama.model,
|
||||
// prompt: prompt,
|
||||
// stream: true,
|
||||
// raw: true,
|
||||
// options: { stop: options?.stopTokens }
|
||||
// })
|
||||
// }
|
||||
|
||||
// return ollama.chat({
|
||||
// model: voidConfig.ollama.model,
|
||||
// messages: messages,
|
||||
// stream: true,
|
||||
// options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) }
|
||||
// });
|
||||
// })
|
||||
// .then(async stream => {
|
||||
// if (!stream) return;
|
||||
|
||||
// abortRef.current = () => {
|
||||
// didAbort = true
|
||||
// stream.abort()
|
||||
// }
|
||||
// for await (const chunk of stream) {
|
||||
// if (didAbort) return;
|
||||
|
||||
// const newText = (mode === 'fim'
|
||||
// ? (chunk as GenerateResponse).response
|
||||
// : (chunk as ChatResponse).message.content
|
||||
// )
|
||||
// fullText += newText;
|
||||
// onText(newText, fullText);
|
||||
// }
|
||||
// onFinalMessage(fullText);
|
||||
// })
|
||||
// .catch(error => {
|
||||
// // Check if the error is a connection error
|
||||
// if (error instanceof Error && error.message.includes('Failed to fetch')) {
|
||||
// const errorMessage = 'Ollama service is not running. Please start the Ollama service and try again.';
|
||||
// onText(errorMessage, errorMessage);
|
||||
// onFinalMessage(errorMessage);
|
||||
// } else if (error) {
|
||||
// onError(error);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
// // Greptile
|
||||
// // https://docs.greptile.com/api-reference/query
|
||||
// // https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
// const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
// let didAbort = false
|
||||
// let fullText = ''
|
||||
|
||||
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
// abortRef.current = () => {
|
||||
// didAbort = true
|
||||
// }
|
||||
|
||||
// fetch('https://api.greptile.com/v2/query', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// "Authorization": `Bearer ${voidConfig.greptile.apikey}`,
|
||||
// "X-Github-Token": `${voidConfig.greptile.githubPAT}`,
|
||||
// "Content-Type": `application/json`,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// messages,
|
||||
// stream: true,
|
||||
// repositories: [voidConfig.greptile.repoinfo],
|
||||
// }),
|
||||
// })
|
||||
// // this is {message}\n{message}\n{message}...\n
|
||||
// .then(async response => {
|
||||
// const text = await response.text()
|
||||
// console.log('got greptile', text)
|
||||
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
// })
|
||||
// // TODO make this actually stream, right now it just sends one message at the end
|
||||
// .then(async responseArr => {
|
||||
// if (didAbort)
|
||||
// return
|
||||
|
||||
// for (let response of responseArr) {
|
||||
|
||||
// const type: string = response['type']
|
||||
// const message = response['message']
|
||||
|
||||
// // when receive text
|
||||
// if (type === 'message') {
|
||||
// fullText += message
|
||||
// onText(message, fullText)
|
||||
// }
|
||||
// else if (type === 'sources') {
|
||||
// const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null }
|
||||
// fullText += filepath
|
||||
// onText(filepath, fullText)
|
||||
// }
|
||||
// // type: 'status' with an empty 'message' means last message
|
||||
// else if (type === 'status') {
|
||||
// if (!message) {
|
||||
// onFinalMessage(fullText)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// })
|
||||
// .catch(e => {
|
||||
// onError(e)
|
||||
// });
|
||||
|
||||
// }
|
||||
|
||||
// export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ options, mode, messages, fimInfo, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
// if (!voidConfig)
|
||||
// return onError('No config file found for LLM.');
|
||||
|
||||
// // handle defaults
|
||||
// if (!mode) mode = 'chat'
|
||||
// if (!messages) messages = []
|
||||
|
||||
// // build messages
|
||||
// if (mode === 'chat') {
|
||||
// // nothing needed
|
||||
// } else if (mode === 'fim') {
|
||||
// fimInfo = fimInfo!
|
||||
|
||||
// const system = getFIMSystem({ voidConfig, fimInfo })
|
||||
// const prompt = getFIMPrompt({ voidConfig, fimInfo })
|
||||
// messages = ([
|
||||
// { role: 'system', content: system },
|
||||
// { role: 'user', content: prompt }
|
||||
// ] as const)
|
||||
|
||||
// }
|
||||
|
||||
// // trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
// messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
// .filter(m => m.content !== '')
|
||||
|
||||
// if (messages.length === 0)
|
||||
// return onError('No messages provided to LLM.');
|
||||
|
||||
// switch (voidConfig.default.whichApi) {
|
||||
// case 'anthropic':
|
||||
// return sendAnthropicMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// case 'openAI':
|
||||
// case 'openRouter':
|
||||
// case 'openAICompatible':
|
||||
// return sendOpenAIMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// case 'gemini':
|
||||
// return sendGeminiMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// case 'ollama':
|
||||
// return sendOllamaMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// case 'greptile':
|
||||
// return sendGreptileMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// default:
|
||||
// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
// }
|
||||
|
||||
// }
|
||||
@@ -1,67 +1,212 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ConfigState } from '../../../registerConfig.js'
|
||||
import { VoidSidebarState, ReactServicesType } from '../../../registerSidebar.js'
|
||||
import { ThreadsState } from '../../../registerThreads.js'
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { ThreadsState } from '../../../threadHistoryService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { VoidQuickEditState } from '../../../quickEditStateService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js';
|
||||
import { IContextViewService, IContextMenuService } from '../../../../../../../platform/contextview/browser/contextView.js';
|
||||
import { IFileService } from '../../../../../../../platform/files/common/files.js';
|
||||
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
|
||||
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
|
||||
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
|
||||
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
|
||||
import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'
|
||||
import { INotificationService } from '../../../../../../../platform/notification/common/notification.js'
|
||||
import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'
|
||||
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js'
|
||||
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'
|
||||
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
||||
let services: ReactServicesType
|
||||
// even if React hasn't mounted yet, the variables are always updated to the latest state.
|
||||
// React listens by adding a setState function to these listeners.
|
||||
let quickEditState: VoidQuickEditState
|
||||
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
|
||||
|
||||
// even if React hasn't mounted yet, these variables are always updated to the latest state:
|
||||
let sidebarState: VoidSidebarState
|
||||
let configState: ConfigState
|
||||
let threadsState: ThreadsState
|
||||
|
||||
// React listens by adding a setState function to these:
|
||||
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
|
||||
const configStateListeners: Set<(s: ConfigState) => void> = new Set()
|
||||
|
||||
let threadsState: ThreadsState
|
||||
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
|
||||
|
||||
let settingsState: VoidSettingsState
|
||||
const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set()
|
||||
|
||||
let refreshModelState: RefreshModelStateOfProvider
|
||||
const refreshModelStateListeners: Set<(s: RefreshModelStateOfProvider) => void> = new Set()
|
||||
const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: RefreshModelStateOfProvider) => void> = new Set()
|
||||
|
||||
let colorThemeState: ColorScheme
|
||||
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
|
||||
|
||||
// must call this before you can use any of the hooks below
|
||||
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
|
||||
|
||||
let wasCalled = false
|
||||
export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
|
||||
export const _registerServices = (services_: ReactServicesType) => {
|
||||
const disposables: IDisposable[] = []
|
||||
|
||||
if (wasCalled) console.error(`void _registerServices was called again! It should only be called once.`)
|
||||
// don't register services twice
|
||||
if (wasCalled) {
|
||||
return
|
||||
// console.error(`⚠️ Void _registerServices was called again! It should only be called once.`)
|
||||
}
|
||||
wasCalled = true
|
||||
|
||||
services = services_
|
||||
const { sidebarStateService, configStateService, threadsStateService, } = services
|
||||
_registerAccessor(accessor)
|
||||
|
||||
const stateServices = {
|
||||
quickEditStateService: accessor.get(IQuickEditStateService),
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
threadsStateService: accessor.get(IThreadHistoryService),
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
themeService: accessor.get(IThemeService),
|
||||
}
|
||||
|
||||
const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices
|
||||
|
||||
quickEditState = quickEditStateService.state
|
||||
disposables.push(
|
||||
quickEditStateService.onDidChangeState(() => {
|
||||
quickEditState = quickEditStateService.state
|
||||
quickEditStateListeners.forEach(l => l(quickEditState))
|
||||
})
|
||||
)
|
||||
|
||||
sidebarState = sidebarStateService.state
|
||||
sidebarStateService.onDidChangeState(() => {
|
||||
sidebarState = sidebarStateService.state
|
||||
sidebarStateListeners.forEach(l => l(sidebarState))
|
||||
})
|
||||
|
||||
configState = configStateService.state
|
||||
configStateService.onDidChangeState(() => {
|
||||
configState = configStateService.state
|
||||
configStateListeners.forEach(l => l(configState))
|
||||
})
|
||||
disposables.push(
|
||||
sidebarStateService.onDidChangeState(() => {
|
||||
sidebarState = sidebarStateService.state
|
||||
sidebarStateListeners.forEach(l => l(sidebarState))
|
||||
})
|
||||
)
|
||||
|
||||
threadsState = threadsStateService.state
|
||||
threadsStateService.onDidChangeCurrentThread(() => {
|
||||
threadsState = threadsStateService.state
|
||||
threadsStateListeners.forEach(l => l(threadsState))
|
||||
})
|
||||
disposables.push(
|
||||
threadsStateService.onDidChangeCurrentThread(() => {
|
||||
threadsState = threadsStateService.state
|
||||
threadsStateListeners.forEach(l => l(threadsState))
|
||||
})
|
||||
)
|
||||
|
||||
settingsState = settingsStateService.state
|
||||
disposables.push(
|
||||
settingsStateService.onDidChangeState(() => {
|
||||
settingsState = settingsStateService.state
|
||||
settingsStateListeners.forEach(l => l(settingsState))
|
||||
})
|
||||
)
|
||||
|
||||
refreshModelState = refreshModelService.state
|
||||
disposables.push(
|
||||
refreshModelService.onDidChangeState((providerName) => {
|
||||
refreshModelState = refreshModelService.state
|
||||
refreshModelStateListeners.forEach(l => l(refreshModelState))
|
||||
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState))
|
||||
})
|
||||
)
|
||||
|
||||
colorThemeState = themeService.getColorTheme().type
|
||||
disposables.push(
|
||||
themeService.onDidColorThemeChange(theme => {
|
||||
colorThemeState = theme.type
|
||||
colorThemeStateListeners.forEach(l => l(colorThemeState))
|
||||
})
|
||||
)
|
||||
|
||||
return disposables
|
||||
}
|
||||
|
||||
|
||||
const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
const reactAccessor = {
|
||||
IModelService: accessor.get(IModelService),
|
||||
IClipboardService: accessor.get(IClipboardService),
|
||||
IContextViewService: accessor.get(IContextViewService),
|
||||
IContextMenuService: accessor.get(IContextMenuService),
|
||||
IFileService: accessor.get(IFileService),
|
||||
IHoverService: accessor.get(IHoverService),
|
||||
IThemeService: accessor.get(IThemeService),
|
||||
ILLMMessageService: accessor.get(ILLMMessageService),
|
||||
IRefreshModelService: accessor.get(IRefreshModelService),
|
||||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IInlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
IQuickEditStateService: accessor.get(IQuickEditStateService),
|
||||
ISidebarStateService: accessor.get(ISidebarStateService),
|
||||
IThreadHistoryService: accessor.get(IThreadHistoryService),
|
||||
|
||||
IInstantiationService: accessor.get(IInstantiationService),
|
||||
ICodeEditorService: accessor.get(ICodeEditorService),
|
||||
ICommandService: accessor.get(ICommandService),
|
||||
IContextKeyService: accessor.get(IContextKeyService),
|
||||
INotificationService: accessor.get(INotificationService),
|
||||
IAccessibilityService: accessor.get(IAccessibilityService),
|
||||
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
|
||||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
}
|
||||
|
||||
type ReactAccessor = ReturnType<typeof getReactAccessor>
|
||||
|
||||
|
||||
let reactAccessor_: ReactAccessor | null = null
|
||||
const _registerAccessor = (accessor: ServicesAccessor) => {
|
||||
const reactAccessor = getReactAccessor(accessor)
|
||||
reactAccessor_ = reactAccessor
|
||||
}
|
||||
|
||||
// -- services --
|
||||
export const useService = <T extends keyof ReactServicesType,>(serviceName: T) => {
|
||||
if (services === null) {
|
||||
throw new Error('useAccessor must be used within an AccessorProvider')
|
||||
export const useAccessor = () => {
|
||||
if (!reactAccessor_) {
|
||||
throw new Error(`⚠️ Void useAccessor was called before _registerServices!`)
|
||||
}
|
||||
return services[serviceName] as ReactServicesType[T]
|
||||
|
||||
return { get: <S extends keyof ReactAccessor,>(service: S): ReactAccessor[S] => reactAccessor_![service] }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -- state of services --
|
||||
|
||||
export const useQuickEditState = () => {
|
||||
const [s, ss] = useState(quickEditState)
|
||||
useEffect(() => {
|
||||
ss(quickEditState)
|
||||
quickEditStateListeners.add(ss)
|
||||
return () => { quickEditStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
export const useSidebarState = () => {
|
||||
const [s, ss] = useState(sidebarState)
|
||||
useEffect(() => {
|
||||
@@ -72,12 +217,12 @@ export const useSidebarState = () => {
|
||||
return s
|
||||
}
|
||||
|
||||
export const useConfigState = () => {
|
||||
const [s, ss] = useState(configState)
|
||||
export const useSettingsState = () => {
|
||||
const [s, ss] = useState(settingsState)
|
||||
useEffect(() => {
|
||||
ss(configState)
|
||||
configStateListeners.add(ss)
|
||||
return () => { configStateListeners.delete(ss) }
|
||||
ss(settingsState)
|
||||
settingsStateListeners.add(ss)
|
||||
return () => { settingsStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
@@ -91,3 +236,37 @@ export const useThreadsState = () => {
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
export const useRefreshModelState = () => {
|
||||
const [s, ss] = useState(refreshModelState)
|
||||
useEffect(() => {
|
||||
ss(refreshModelState)
|
||||
refreshModelStateListeners.add(ss)
|
||||
return () => { refreshModelStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
export const useRefreshModelListener = (listener: (providerName: RefreshableProviderName, s: RefreshModelStateOfProvider) => void) => {
|
||||
useEffect(() => {
|
||||
refreshModelProviderListeners.add(listener)
|
||||
return () => { refreshModelProviderListeners.delete(listener) }
|
||||
}, [listener])
|
||||
}
|
||||
|
||||
|
||||
export const useIsDark = () => {
|
||||
const [s, ss] = useState(colorThemeState)
|
||||
useEffect(() => {
|
||||
ss(colorThemeState)
|
||||
colorThemeStateListeners.add(ss)
|
||||
return () => { colorThemeStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
|
||||
// s is the theme, return isDark instead of s
|
||||
const isDark = s === ColorScheme.DARK || s === ColorScheme.HIGH_CONTRAST_DARK
|
||||
return isDark
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
|
||||
import { VoidSelectBox } from '../util/inputs.js'
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
|
||||
const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
let weChangedText = false
|
||||
|
||||
return <VoidSelectBox
|
||||
options={settingsState._modelOptions}
|
||||
onChangeSelection={useCallback((newVal: ModelSelection) => {
|
||||
if (weChangedText) return
|
||||
voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
|
||||
}, [voidSettingsService, featureName])}
|
||||
// we are responsible for setting the initial state here. always sync instance when state changes.
|
||||
onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
const syncInstance = () => {
|
||||
const modelsListRef = voidSettingsService.state._modelOptions // as a ref
|
||||
const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
|
||||
weChangedText = true
|
||||
instance.select(selectionIdx === -1 ? 0 : selectionIdx)
|
||||
weChangedText = false
|
||||
}
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
}, [voidSettingsService, featureName])}
|
||||
/>
|
||||
}
|
||||
|
||||
const DummySelectBox = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const comandService = accessor.get('ICommandService')
|
||||
|
||||
const openSettings = () => {
|
||||
comandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
|
||||
};
|
||||
|
||||
return <div
|
||||
className={`
|
||||
flex items-center
|
||||
flex-nowrap text-ellipsis
|
||||
text-vscode-charts-yellow
|
||||
hover:brightness-90 transition-all duration-200
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={openSettings}
|
||||
>
|
||||
<IconWarning
|
||||
size={20}
|
||||
className='mr-1 brightness-90'
|
||||
/>
|
||||
<span>Model required</span>
|
||||
</div>
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
// onChangeSelection={() => { }}
|
||||
// />
|
||||
}
|
||||
|
||||
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
return <>
|
||||
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <ModelSelectBox featureName={featureName} />}
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
return <div className='flex items-center py-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-200/10'>
|
||||
<button className='flex items-center' disabled={disabled} onClick={onClick}>
|
||||
{icon}
|
||||
</button>
|
||||
<span className='opacity-50'>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
// models
|
||||
const RefreshModelButton = ({ providerName }: { providerName: RefreshableProviderName }) => {
|
||||
const refreshModelState = useRefreshModelState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const refreshModelService = accessor.get('IRefreshModelService')
|
||||
|
||||
const [justFinished, setJustSucceeded] = useState(false)
|
||||
|
||||
useRefreshModelListener(
|
||||
useCallback((providerName2, refreshModelState) => {
|
||||
if (providerName2 !== providerName) return
|
||||
const { state } = refreshModelState[providerName]
|
||||
if (state !== 'success') return
|
||||
// now we know we just entered 'success' state for this providerName
|
||||
setJustSucceeded(true)
|
||||
const tid = setTimeout(() => { setJustSucceeded(false) }, 2000)
|
||||
return () => clearTimeout(tid)
|
||||
}, [providerName])
|
||||
)
|
||||
|
||||
const { state } = refreshModelState[providerName]
|
||||
const isRefreshing = state === 'refreshing'
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
return <SubtleButton
|
||||
onClick={() => { refreshModelService.refreshModels(providerName) }}
|
||||
text={justFinished ? `${providerTitle} Models are up-to-date!` : `Refresh Models List for ${providerTitle}.`}
|
||||
icon={isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
|
||||
disabled={isRefreshing || justFinished}
|
||||
/>
|
||||
}
|
||||
|
||||
const RefreshableModels = () => {
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
|
||||
const buttons = refreshableProviderNames.map(providerName => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
|
||||
return <RefreshModelButton key={providerName} providerName={providerName} />
|
||||
})
|
||||
|
||||
return <>
|
||||
{buttons}
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const modelNameRef = useRef<string | null>(null)
|
||||
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
|
||||
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: displayInfoOfProviderName(providerName).title, value: providerName })), [providerNames])
|
||||
|
||||
return <>
|
||||
<div className='flex items-center gap-4'>
|
||||
{/* model */}
|
||||
<div className='max-w-40 w-full'>
|
||||
<VoidInputBox
|
||||
placeholder='Model Name'
|
||||
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
|
||||
multiline={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* provider */}
|
||||
<div className='max-w-40 w-full'>
|
||||
<VoidSelectBox
|
||||
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
|
||||
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
|
||||
options={providerOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* button */}
|
||||
<div className='max-w-40'>
|
||||
<button
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={() => {
|
||||
const providerName = providerNameRef.current
|
||||
const modelName = modelNameRef.current
|
||||
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.')
|
||||
return
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists under ${providerName}.`)
|
||||
return
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
onSubmit()
|
||||
|
||||
}}>Add model</button>
|
||||
</div>
|
||||
|
||||
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
|
||||
{errorString}
|
||||
</div>}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
const AddModelMenuFull = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return <div className='hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden '>
|
||||
{open ?
|
||||
<AddModelMenu onSubmit={() => { setOpen(false) }} />
|
||||
: <button
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={() => setOpen(true)}
|
||||
>Add Model</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
export const ModelDump = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
// a dump of all the enabled providers' models
|
||||
const modelDump: (VoidModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
for (let providerName of providerNames) {
|
||||
const providerSettings = settingsState.settingsOfProvider[providerName]
|
||||
// if (!providerSettings.enabled) continue
|
||||
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._enabled })))
|
||||
}
|
||||
|
||||
// sort by hidden
|
||||
modelDump.sort((a, b) => {
|
||||
return Number(b.providerEnabled) - Number(a.providerEnabled)
|
||||
})
|
||||
|
||||
return <div className=''>
|
||||
{modelDump.map(m => {
|
||||
const { isHidden, isDefault, modelName, providerName, providerEnabled } = m
|
||||
|
||||
const disabled = !providerEnabled
|
||||
|
||||
return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden cursor-default'>
|
||||
{/* left part is width:full */}
|
||||
<div className={`w-full flex items-center gap-4`}>
|
||||
<span>{`${modelName} (${providerName})`}</span>
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className='w-fit flex items-center gap-4'>
|
||||
<span className='opacity-50 whitespace-nowrap'>{isDefault ? '' : '(custom model)'}</span>
|
||||
|
||||
<VoidSwitch
|
||||
value={disabled ? false : !isHidden}
|
||||
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
|
||||
disabled={disabled}
|
||||
size='sm'
|
||||
/>
|
||||
|
||||
<div className={`w-5 flex items-center justify-center`}>
|
||||
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
// providers
|
||||
|
||||
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
|
||||
|
||||
|
||||
const { title: providerTitle, } = displayInfoOfProviderName(providerName)
|
||||
|
||||
const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
let weChangedTextRef = false
|
||||
|
||||
return <ErrorBoundary>
|
||||
<div className='my-1'>
|
||||
<VoidInputBox
|
||||
placeholder={`${providerTitle} ${settingTitle} (${placeholder}).`}
|
||||
onChangeText={useCallback((newVal) => {
|
||||
if (weChangedTextRef) return
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
|
||||
// we are responsible for setting the initial value. always sync the instance whenever there's a change to state.
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
const syncInstance = () => {
|
||||
const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName];
|
||||
const stateVal = settingsAtProvider[settingName as SettingName]
|
||||
|
||||
// console.log('SYNCING TO', providerName, settingName, stateVal)
|
||||
weChangedTextRef = true
|
||||
instance.value = stateVal as string
|
||||
weChangedTextRef = false
|
||||
|
||||
const isEverySettingPresent = Object.keys(defaultProviderSettings[providerName]).every(key => {
|
||||
return !!settingsAtProvider[key as keyof typeof settingsAtProvider]
|
||||
})
|
||||
|
||||
const shouldEnable = isEverySettingPresent && !settingsAtProvider._enabled // enable if all settings are present and not already enabled
|
||||
const shouldDisable = !isEverySettingPresent && settingsAtProvider._enabled
|
||||
|
||||
if (shouldEnable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
}
|
||||
|
||||
if (shouldDisable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
|
||||
}
|
||||
|
||||
}
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
multiline={false}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-xs'>
|
||||
<ChatMarkdownRender string={subTextMd} />
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
// const voidSettingsState = useSettingsState()
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// const { enabled } = voidSettingsState.settingsOfProvider[providerName]
|
||||
const settingNames = customSettingNamesOfProvider(providerName)
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
|
||||
return <div className='my-4'>
|
||||
|
||||
<div className='flex items-center w-full gap-4'>
|
||||
<h3 className='text-xl truncate'>{providerTitle}</h3>
|
||||
|
||||
{/* enable provider switch */}
|
||||
{/* <VoidSwitch
|
||||
value={!!enabled}
|
||||
onChange={
|
||||
useCallback(() => {
|
||||
const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled
|
||||
voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
|
||||
}, [voidSettingsService, providerName])}
|
||||
size='sm+'
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<div className='px-0'>
|
||||
{/* settings besides models (e.g. api key) */}
|
||||
{settingNames.map((settingName, i) => {
|
||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
})}
|
||||
</div>
|
||||
</div >
|
||||
}
|
||||
|
||||
|
||||
export const VoidProviderSettings = () => {
|
||||
return <>
|
||||
{providerNames.map(providerName =>
|
||||
<SettingsForProvider key={providerName} providerName={providerName} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
// export const VoidFeatureFlagSettings = () => {
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// const voidSettingsState = useSettingsState()
|
||||
|
||||
// return <>
|
||||
// {featureFlagNames.map((flagName) => {
|
||||
// const value = voidSettingsState.featureFlagSettings[flagName]
|
||||
// const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
// return <div key={flagName} className='hover:bg-black/10 hover:dark:bg-gray-200/10 rounded-sm overflow-hidden py-1 px-3 my-1'>
|
||||
// <div className='flex items-center'>
|
||||
// <VoidCheckBox
|
||||
// label=''
|
||||
// value={value}
|
||||
// onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}
|
||||
// />
|
||||
// <h4 className='text-sm'>{description}</h4>
|
||||
// </div>
|
||||
// </div>
|
||||
// })}
|
||||
// </>
|
||||
// }
|
||||
export const VoidFeatureFlagSettings = () => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
return featureFlagNames.map((flagName) => {
|
||||
|
||||
const enabled = voidSettingsState.featureFlagSettings[flagName]
|
||||
const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
|
||||
return <SubtleButton
|
||||
onClick={() => { voidSettingsService.setFeatureFlag(flagName, !enabled) }}
|
||||
text={description}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// full settings
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark()
|
||||
|
||||
const [tab, setTab] = useState<'models' | 'features'>('models')
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div className='w-full h-full px-10 py-10 select-none'>
|
||||
|
||||
<div className='max-w-5xl mx-auto'>
|
||||
|
||||
<h1 className='text-2xl w-full'>Void Settings</h1>
|
||||
|
||||
{/* separator */}
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
<div className='flex items-stretch'>
|
||||
|
||||
{/* tabs */}
|
||||
<div className='flex flex-col w-full max-w-32'>
|
||||
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('models') }}
|
||||
>Models</button>
|
||||
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('features') }}
|
||||
>Features</button>
|
||||
</div>
|
||||
|
||||
{/* separator */}
|
||||
<div className='w-[1px] mx-4' />
|
||||
|
||||
|
||||
{/* content */}
|
||||
<div className='w-full overflow-y-auto'>
|
||||
|
||||
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`}>Providers</h2>
|
||||
<ErrorBoundary>
|
||||
<VoidFeatureFlagSettings />
|
||||
<VoidProviderSettings />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-4`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
|
||||
{/* <VoidFeatureFlagSettings /> */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { Settings } from './Settings.js'
|
||||
|
||||
export const mountVoidSettings = mountFnGenerator(Settings)
|
||||
|
||||
|
||||
@@ -1,32 +1,129 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: 'selector', // '{prefix-}dark' className is used to identify `dark:`
|
||||
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
|
||||
theme: {
|
||||
extend: {
|
||||
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
colors: {
|
||||
vscode: {
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
"editor-bg": "var(--vscode-editor-background)",
|
||||
"editor-fg": "var(--vscode-editor-foreground)",
|
||||
// see: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
|
||||
// base colors
|
||||
"fg": "var(--vscode-foreground)",
|
||||
"focus-border": "var(--vscode-focusBorder)",
|
||||
"disabled-fg": "var(--vscode-disabledForeground)",
|
||||
"widget-border": "var(--vscode-widget-border)",
|
||||
"widget-shadow": "var(--vscode-widget-shadow)",
|
||||
"selection-bg": "var(--vscode-selection-background)",
|
||||
"description-fg": "var(--vscode-descriptionForeground)",
|
||||
"error-fg": "var(--vscode-errorForeground)",
|
||||
"icon-fg": "var(--vscode-icon-foreground)",
|
||||
"sash-hover-border": "var(--vscode-sash-hoverBorder)",
|
||||
|
||||
// text colors
|
||||
"text-blockquote-bg": "var(--vscode-textBlockQuote-background)",
|
||||
"text-blockquote-border": "var(--vscode-textBlockQuote-border)",
|
||||
"text-codeblock-bg": "var(--vscode-textCodeBlock-background)",
|
||||
"text-link-active-fg": "var(--vscode-textLink-activeForeground)",
|
||||
"text-link-fg": "var(--vscode-textLink-foreground)",
|
||||
"text-preformat-fg": "var(--vscode-textPreformat-foreground)",
|
||||
"text-preformat-bg": "var(--vscode-textPreformat-background)",
|
||||
"text-separator-fg": "var(--vscode-textSeparator-foreground)",
|
||||
|
||||
// input colors
|
||||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"button-fg": "var(--vscode-button-foreground)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-placeholder-fg": "var(--vscode-placeholderForeground)",
|
||||
"input-active-bg": "var(--vscode-activeBackground)",
|
||||
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
|
||||
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
|
||||
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",
|
||||
"input-validation-error-bg": "var(--vscode-inputValidation-errorBackground)",
|
||||
"input-validation-error-fg": "var(--vscode-inputValidation-errorForeground)",
|
||||
"input-validation-error-border": "var(--vscode-inputValidation-errorBorder)",
|
||||
"input-validation-info-bg": "var(--vscode-inputValidation-infoBackground)",
|
||||
"input-validation-info-fg": "var(--vscode-inputValidation-infoForeground)",
|
||||
"input-validation-info-border": "var(--vscode-inputValidation-infoBorder)",
|
||||
"input-validation-warning-bg": "var(--vscode-inputValidation-warningBackground)",
|
||||
"input-validation-warning-fg": "var(--vscode-inputValidation-warningForeground)",
|
||||
"input-validation-warning-border": "var(--vscode-inputValidation-warningBorder)",
|
||||
|
||||
// command center colors (the top bar)
|
||||
"commandcenter-fg": "var(--vscode-commandCenter-foreground)",
|
||||
"commandcenter-active-fg": "var(--vscode-commandCenter-activeForeground)",
|
||||
"commandcenter-bg": "var(--vscode-commandCenter-background)",
|
||||
"commandcenter-active-bg": "var(--vscode-commandCenter-activeBackground)",
|
||||
"commandcenter-border": "var(--vscode-commandCenter-border)",
|
||||
"commandcenter-inactive-fg": "var(--vscode-commandCenter-inactiveForeground)",
|
||||
"commandcenter-inactive-border": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
"commandcenter-active-border": "var(--vscode-commandCenter-activeBorder)",
|
||||
"commandcenter-debugging-bg": "var(--vscode-commandCenter-debuggingBackground)",
|
||||
|
||||
// badge colors
|
||||
"badge-fg": "var(--vscode-badge-foreground)",
|
||||
"badge-bg": "var(--vscode-badge-background)",
|
||||
|
||||
// button colors
|
||||
"button-bg": "var(--vscode-button-background)",
|
||||
"button-hoverBg": "var(--vscode-button-hoverBackground)",
|
||||
"button-fg": "var(--vscode-button-foreground)",
|
||||
"button-border": "var(--vscode-button-border)",
|
||||
"button-separator": "var(--vscode-button-separator)",
|
||||
"button-hover-bg": "var(--vscode-button-hoverBackground)",
|
||||
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
|
||||
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
|
||||
"button-secondary-hoverBg": "var(--vscode-button-secondaryHoverBackground)",
|
||||
"dropdown-bg": "var(--vscode-settings-dropdownBackground)",
|
||||
"dropdown-foreground": "var(--vscode-settings-dropdownForeground)",
|
||||
"dropdown-border": "var(--vscode-settings-dropdownBorder)",
|
||||
"focus-border": "var(--vscode-focusBorder)",
|
||||
"button-secondary-hover-bg": "var(--vscode-button-secondaryHoverBackground)",
|
||||
|
||||
// checkbox colors
|
||||
"checkbox-bg": "var(--vscode-checkbox-background)",
|
||||
"checkbox-fg": "var(--vscode-checkbox-foreground)",
|
||||
"checkbox-border": "var(--vscode-checkbox-border)",
|
||||
"checkbox-select-bg": "var(--vscode-checkbox-selectBackground)",
|
||||
|
||||
// sidebar colors
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
"sidebar-fg": "var(--vscode-sideBar-foreground)",
|
||||
"sidebar-border": "var(--vscode-sideBar-border)",
|
||||
"sidebar-drop-backdrop": "var(--vscode-sideBar-dropBackground)",
|
||||
"sidebar-title-fg": "var(--vscode-sideBarTitle-foreground)",
|
||||
"sidebar-header-bg": "var(--vscode-sideBarSectionHeader-background)",
|
||||
"sidebar-header-fg": "var(--vscode-sideBarSectionHeader-foreground)",
|
||||
"sidebar-header-border": "var(--vscode-sideBarSectionHeader-border)",
|
||||
"sidebar-activitybartop-border": "var(--vscode-sideBarActivityBarTop-border)",
|
||||
"sidebar-title-bg": "var(--vscode-sideBarTitle-background)",
|
||||
"sidebar-title-border": "var(--vscode-sideBarTitle-border)",
|
||||
"sidebar-stickyscroll-bg": "var(--vscode-sideBarStickyScroll-background)",
|
||||
"sidebar-stickyscroll-border": "var(--vscode-sideBarStickyScroll-border)",
|
||||
"sidebar-stickyscroll-shadow": "var(--vscode-sideBarStickyScroll-shadow)",
|
||||
|
||||
// other colors (these are partially complete)
|
||||
|
||||
// editor colors
|
||||
"editor-bg": "var(--vscode-editor-background)",
|
||||
"editor-fg": "var(--vscode-editor-foreground)",
|
||||
|
||||
|
||||
|
||||
// other
|
||||
"editorwidget-bg": "var(--vscode-editorWidget-background)",
|
||||
"toolbar-hover-bg": "var(--vscode-toolbar-hoverBackground)",
|
||||
"toolbar-foreground": "var(--vscode-editorActionList-foreground)",
|
||||
|
||||
"editorwidget-fg": "var(--vscode-editorWidget-foreground)",
|
||||
"editorwidget-border": "var(--vscode-editorWidget-border)",
|
||||
|
||||
"charts-orange": "var(--vscode-charts-orange)",
|
||||
"charts-yellow": "var(--vscode-charts-yellow)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
prefix: 'prefix-'
|
||||
prefix: 'void-'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: [
|
||||
'./src2/sidebar-tsx/Sidebar.tsx',
|
||||
'./src2/util/sendLLMMessage.tsx',
|
||||
'./src2/util/posthog.tsx',
|
||||
'./src2/util/diffLines.tsx',
|
||||
'./src2/sidebar-tsx/index.tsx',
|
||||
'./src2/void-settings-tsx/index.tsx',
|
||||
'./src2/ctrl-k-tsx/index.tsx',
|
||||
'./src2/diff/index.tsx',
|
||||
],
|
||||
outDir: './out',
|
||||
format: ['esm'],
|
||||
@@ -14,7 +19,7 @@ export default defineConfig({
|
||||
// dts: true,
|
||||
// sourcemap: true,
|
||||
|
||||
clean: true,
|
||||
clean: false,
|
||||
platform: 'browser', // 'node'
|
||||
target: 'esnext',
|
||||
injectStyle: true, // bundle css into the output file
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IEncryptionService } from '../../../../platform/encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
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 nonDefaultConfigFields = [
|
||||
'anthropic',
|
||||
'openAI',
|
||||
'gemini',
|
||||
'greptile',
|
||||
'ollama',
|
||||
'openRouter',
|
||||
'openAICompatible',
|
||||
'azure',
|
||||
] as const
|
||||
|
||||
|
||||
|
||||
const voidConfigInfo: Record<
|
||||
typeof nonDefaultConfigFields[number] | 'default', {
|
||||
[prop: string]: {
|
||||
description: string;
|
||||
enumArr?: readonly string[] | undefined;
|
||||
defaultVal: string;
|
||||
};
|
||||
}
|
||||
> = {
|
||||
default: {
|
||||
whichApi: configEnum(
|
||||
'API Provider.',
|
||||
'anthropic',
|
||||
nonDefaultConfigFields,
|
||||
),
|
||||
|
||||
maxTokens: configEnum(
|
||||
'Max number of tokens to output.',
|
||||
'1024',
|
||||
[
|
||||
'default', // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
|
||||
'1024',
|
||||
'2048',
|
||||
'4096',
|
||||
'8192'
|
||||
] as const,
|
||||
),
|
||||
|
||||
},
|
||||
anthropic: {
|
||||
apikey: configString('Anthropic API key.', ''),
|
||||
model: configEnum(
|
||||
'Anthropic model to use.',
|
||||
'claude-3-5-sonnet-20240620',
|
||||
[
|
||||
'claude-3-5-sonnet-20240620',
|
||||
'claude-3-opus-20240229',
|
||||
'claude-3-sonnet-20240229',
|
||||
'claude-3-haiku-20240307'
|
||||
] as const,
|
||||
),
|
||||
},
|
||||
openAI: {
|
||||
apikey: configString('OpenAI API key.', ''),
|
||||
model: configEnum(
|
||||
'OpenAI model to use.',
|
||||
'gpt-4o',
|
||||
[
|
||||
'o1-preview',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-2024-05-13',
|
||||
'gpt-4o-2024-08-06',
|
||||
'gpt-4o-mini',
|
||||
'gpt-4o-mini-2024-07-18',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4-turbo-2024-04-09',
|
||||
'gpt-4-turbo-preview',
|
||||
'gpt-4-0125-preview',
|
||||
'gpt-4-1106-preview',
|
||||
'gpt-4',
|
||||
'gpt-4-0613',
|
||||
'gpt-3.5-turbo-0125',
|
||||
'gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-1106'
|
||||
] as const
|
||||
),
|
||||
},
|
||||
greptile: {
|
||||
apikey: configString('Greptile API key.', ''),
|
||||
githubPAT: configString('Github PAT that Greptile uses to access your repository', ''),
|
||||
remote: configEnum(
|
||||
'Repo location',
|
||||
'github',
|
||||
[
|
||||
'github',
|
||||
'gitlab'
|
||||
] as const
|
||||
),
|
||||
repository: configString('Repository identifier in "owner / repository" format.', ''),
|
||||
branch: configString('Name of the branch to use.', 'main'),
|
||||
},
|
||||
ollama: {
|
||||
endpoint: configString(
|
||||
'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode - webview://*" ollama serve`.',
|
||||
'http://127.0.0.1:11434'
|
||||
),
|
||||
model: configEnum(
|
||||
'Ollama model to use.',
|
||||
'codestral',
|
||||
['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', '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.'
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
},
|
||||
gemini: {
|
||||
apikey: configString('Google API key.', ''),
|
||||
model: configEnum(
|
||||
'Gemini model to use.',
|
||||
'gemini-1.5-flash',
|
||||
[
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-1.0-pro'
|
||||
] as const
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// this is the type that comes with metadata like desc, default val, etc
|
||||
export 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 = (partialVoidConfig: PartialVoidConfig): VoidConfig => {
|
||||
const config = {} as PartialVoidConfig
|
||||
for (const field of [...nonDefaultConfigFields, 'default'] as const) {
|
||||
config[field] = {}
|
||||
for (const prop in voidConfigInfo[field]) {
|
||||
config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal
|
||||
}
|
||||
}
|
||||
return config as VoidConfig
|
||||
}
|
||||
|
||||
|
||||
const VOID_CONFIG_KEY = 'void.partialVoidConfig'
|
||||
|
||||
export type SetFieldFnType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => Promise<void>;
|
||||
|
||||
export type ConfigState = {
|
||||
partialVoidConfig: PartialVoidConfig; // free parameter
|
||||
voidConfig: VoidConfig; // computed from partialVoidConfig
|
||||
}
|
||||
|
||||
export interface IVoidConfigStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: ConfigState;
|
||||
readonly voidConfigInfo: VoidConfigInfo;
|
||||
onDidChangeState: Event<void>;
|
||||
setField: SetFieldFnType;
|
||||
}
|
||||
|
||||
export const IVoidConfigStateService = createDecorator<IVoidConfigStateService>('VoidConfigStateService');
|
||||
class VoidConfigStateService extends Disposable implements IVoidConfigStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: ConfigState;
|
||||
readonly voidConfigInfo: VoidConfigInfo = voidConfigInfo; // just putting this here for simplicity, it's static though
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IEncryptionService private readonly _encryptionService: IEncryptionService,
|
||||
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
|
||||
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// at the start, we haven't read the partial config yet, but we need to set state to something, just treat partialVoidConfig like it's empty
|
||||
this.state = {
|
||||
partialVoidConfig: {},
|
||||
voidConfig: getVoidConfig({}),
|
||||
}
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readPartialVoidConfig().then(partialVoidConfig => {
|
||||
this._setState(partialVoidConfig)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private async _readPartialVoidConfig(): Promise<PartialVoidConfig> {
|
||||
const encryptedPartialConfig = this._storageService.get(VOID_CONFIG_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedPartialConfig)
|
||||
return {}
|
||||
|
||||
const partialVoidConfigStr = await this._encryptionService.decrypt(encryptedPartialConfig)
|
||||
return JSON.parse(partialVoidConfigStr)
|
||||
}
|
||||
|
||||
|
||||
private async _storePartialVoidConfig(partialVoidConfig: PartialVoidConfig) {
|
||||
const encryptedPartialConfigStr = await this._encryptionService.encrypt(JSON.stringify(partialVoidConfig))
|
||||
this._storageService.store(VOID_CONFIG_KEY, encryptedPartialConfigStr, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
|
||||
// Set field on PartialVoidConfig
|
||||
setField: SetFieldFnType = async <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => {
|
||||
const { partialVoidConfig } = this.state
|
||||
|
||||
const newPartialConfig: PartialVoidConfig = {
|
||||
...partialVoidConfig,
|
||||
[field]: {
|
||||
...partialVoidConfig[field],
|
||||
[param]: newVal
|
||||
}
|
||||
}
|
||||
await this._storePartialVoidConfig(newPartialConfig)
|
||||
this._setState(newPartialConfig)
|
||||
}
|
||||
|
||||
// internal function to update state, should be called every time state changes
|
||||
private async _setState(partialVoidConfig: PartialVoidConfig) {
|
||||
this.state = {
|
||||
partialVoidConfig: partialVoidConfig,
|
||||
voidConfig: getVoidConfig(partialVoidConfig),
|
||||
}
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidConfigStateService, VoidConfigStateService, InstantiationType.Eager);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,52 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
|
||||
import { posthog } from './react/out/util/posthog.js'
|
||||
|
||||
|
||||
|
||||
// const buildEnv = 'development';
|
||||
// const buildNumber = '1.0.0';
|
||||
// const isMac = process.platform === 'darwin';
|
||||
// // TODO use commandKey
|
||||
// const commandKey = isMac ? '⌘' : 'Ctrl';
|
||||
// const systemInfo = {
|
||||
// buildEnv,
|
||||
// buildNumber,
|
||||
// isMac,
|
||||
// }
|
||||
|
||||
|
||||
|
||||
interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
const IMetricsService = createDecorator<IMetricsService>('metricsService');
|
||||
class MetricsService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
super()
|
||||
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
|
||||
})
|
||||
const deviceId = this._telemetryService.devDeviceId
|
||||
console.debug('deviceId', deviceId)
|
||||
posthog.identify(deviceId)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
@@ -1,246 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import {
|
||||
Extensions as ViewContainerExtensions, IViewContainersRegistry,
|
||||
ViewContainerLocation, IViewsRegistry, Extensions as ViewExtensions,
|
||||
IViewDescriptorService,
|
||||
} from '../../../common/views.js';
|
||||
|
||||
import * as nls from '../../../../nls.js';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
|
||||
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
|
||||
|
||||
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
|
||||
|
||||
import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js';
|
||||
|
||||
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { IThreadHistoryService } from './registerThreads.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
|
||||
// import { IVoidConfigService } from './registerSettings.js';
|
||||
// import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
|
||||
import mountFn from './react/out/sidebar-tsx/Sidebar.js';
|
||||
|
||||
import { IVoidConfigStateService } from './registerConfig.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IInlineDiffsService } from './registerInlineDiffs.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js';
|
||||
|
||||
|
||||
// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
|
||||
|
||||
|
||||
// compare against search.contribution.ts and https://app.greptile.com/chat/w1nsmt3lauwzculipycpn?repo=github%3Amain%3Amicrosoft%2Fvscode
|
||||
// and debug.contribution.ts, scm.contribution.ts (source control)
|
||||
|
||||
export type VoidSidebarState = {
|
||||
isHistoryOpen: boolean;
|
||||
currentTab: 'chat' | 'settings';
|
||||
}
|
||||
|
||||
export type ReactServicesType = {
|
||||
sidebarStateService: IVoidSidebarStateService;
|
||||
configStateService: IVoidConfigStateService;
|
||||
threadsStateService: IThreadHistoryService;
|
||||
fileService: IFileService;
|
||||
modelService: IModelService;
|
||||
inlineDiffService: IInlineDiffsService;
|
||||
sendLLMMessageService: ISendLLMMessageService;
|
||||
}
|
||||
|
||||
// ---------- Define viewpane ----------
|
||||
|
||||
class VoidSidebarViewPane extends ViewPane {
|
||||
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
// Void:
|
||||
// @IVoidSidebarStateService private readonly _voidSidebarStateService: IVoidSidebarStateService,
|
||||
// @IThreadHistoryService private readonly _threadHistoryService: IThreadHistoryService,
|
||||
// TODO chat service
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override renderBody(parent: HTMLElement): void {
|
||||
super.renderBody(parent);
|
||||
|
||||
const { root } = dom.h('div@root')
|
||||
dom.append(parent, root);
|
||||
|
||||
// gets set immediately
|
||||
this.instantiationService.invokeFunction(accessor => {
|
||||
const services: ReactServicesType = {
|
||||
configStateService: accessor.get(IVoidConfigStateService),
|
||||
sidebarStateService: accessor.get(IVoidSidebarStateService),
|
||||
threadsStateService: accessor.get(IThreadHistoryService),
|
||||
fileService: accessor.get(IFileService),
|
||||
modelService: accessor.get(IModelService),
|
||||
inlineDiffService: accessor.get(IInlineDiffsService),
|
||||
sendLLMMessageService: accessor.get(ISendLLMMessageService),
|
||||
}
|
||||
mountFn(root, services);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------- Register viewpane inside the void container ----------
|
||||
|
||||
const voidThemeIcon = Codicon.symbolObject;
|
||||
const voidViewIcon = registerIcon('void-view-icon', voidThemeIcon, localize('voidViewIcon', 'View icon of the Void chat view.'));
|
||||
|
||||
// called VIEWLET_ID in other places for some reason
|
||||
export const VOID_VIEW_CONTAINER_ID = 'workbench.view.void'
|
||||
export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID // not sure if we can change this
|
||||
|
||||
// Register view container
|
||||
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
const viewContainer = viewContainerRegistry.registerViewContainer({
|
||||
id: VOID_VIEW_CONTAINER_ID,
|
||||
title: nls.localize2('void', 'Void'), // this is used to say "Void" (Ctrl + L)
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]),
|
||||
hideIfEmpty: false,
|
||||
icon: voidViewIcon,
|
||||
order: 1,
|
||||
}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true, });
|
||||
|
||||
|
||||
|
||||
// Register search default location to the container (sidebar)
|
||||
const viewsRegistry = Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry);
|
||||
viewsRegistry.registerViews([{
|
||||
id: VOID_VIEW_ID,
|
||||
hideByDefault: false, // start open
|
||||
containerIcon: voidViewIcon,
|
||||
name: nls.localize2('void chat', "Chat"), // this says ... : CHAT
|
||||
ctorDescriptor: new SyncDescriptor(VoidSidebarViewPane),
|
||||
canToggleVisibility: false,
|
||||
canMoveView: true,
|
||||
openCommandActionDescriptor: {
|
||||
id: viewContainer.id,
|
||||
keybindings: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
},
|
||||
order: 1
|
||||
},
|
||||
}], viewContainer);
|
||||
|
||||
|
||||
|
||||
// ---------- Register service that manages sidebar's state ----------
|
||||
|
||||
export interface IVoidSidebarStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: VoidSidebarState; // readonly to the user
|
||||
setState(newState: Partial<VoidSidebarState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
onDidFocusChat: Event<void>;
|
||||
onDidBlurChat: Event<void>;
|
||||
fireFocusChat(): void;
|
||||
fireBlurChat(): void;
|
||||
|
||||
openView(): void;
|
||||
}
|
||||
|
||||
|
||||
export const IVoidSidebarStateService = createDecorator<IVoidSidebarStateService>('voidSidebarStateService');
|
||||
class VoidSidebarStateService extends Disposable implements IVoidSidebarStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onFocusChat = new Emitter<void>();
|
||||
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
|
||||
|
||||
private readonly _onBlurChat = new Emitter<void>();
|
||||
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
|
||||
|
||||
|
||||
// state
|
||||
state: VoidSidebarState
|
||||
|
||||
|
||||
setState(newState: Partial<VoidSidebarState>) {
|
||||
// make sure view is open if the tab changes
|
||||
if ('currentTab' in newState) {
|
||||
this.openView()
|
||||
}
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
openView() {
|
||||
this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
this._viewsService.openView(VOID_VIEW_ID);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IViewsService private readonly _viewsService: IViewsService,
|
||||
// @IThreadHistoryService private readonly _threadHistoryService: IThreadHistoryService,
|
||||
) {
|
||||
super()
|
||||
// auto open the view on mount (if it bothers you this is here, this is technically just initializing the state of the view)
|
||||
this.openView()
|
||||
|
||||
// initial state
|
||||
this.state = {
|
||||
isHistoryOpen: false,
|
||||
currentTab: 'chat',
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidSidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
|
||||
+62
-30
@@ -1,6 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
@@ -11,16 +11,16 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
|
||||
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { CodeStagingSelection, IThreadHistoryService } from './registerThreads.js';
|
||||
// import { IVoidConfigService } from './registerSettings.js';
|
||||
// import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { CodeStagingSelection, IThreadHistoryService } from './threadHistoryService.js';
|
||||
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IVoidSidebarStateService, VOID_VIEW_ID } from './registerSidebar.js';
|
||||
// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
|
||||
import { VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
@@ -51,9 +51,10 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
||||
}
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: 'void.ctrl+l', title: 'Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
@@ -61,27 +62,50 @@ registerAction2(class extends Action2 {
|
||||
if (!model)
|
||||
return
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
|
||||
metricsService.capture('User Action', { type: 'Ctrl+L' })
|
||||
|
||||
const stateService = accessor.get(IVoidSidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
|
||||
// add selection
|
||||
const threadHistoryService = accessor.get(IThreadHistoryService)
|
||||
const currentStaging = threadHistoryService.state._currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s => s.fileURI.fsPath === model.uri.fsPath)
|
||||
|
||||
// if there exists a selection with this URI, replace it
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const selectionRange = roundRangeToLines(
|
||||
accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
editor?.getSelection()
|
||||
)
|
||||
|
||||
|
||||
if (selectionRange) {
|
||||
const selection: CodeStagingSelection = {
|
||||
selectionStr: getContentInRange(model, selectionRange),
|
||||
fileURI: model.uri
|
||||
// select whole lines
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: CodeStagingSelection = selectionStr === null || selectionRange.startLineNumber > selectionRange.endLineNumber ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
}
|
||||
|
||||
// add selection to staging
|
||||
const threadHistoryService = accessor.get(IThreadHistoryService)
|
||||
const currentStaging = threadHistoryService.state._currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s =>
|
||||
s.fileURI.fsPath === model.uri.fsPath
|
||||
&& s.range?.startLineNumber === selection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === selection.range?.endLineNumber
|
||||
)
|
||||
|
||||
// if matches with existing selection, overwrite
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
threadHistoryService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
@@ -89,6 +113,7 @@ registerAction2(class extends Action2 {
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add
|
||||
else {
|
||||
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
|
||||
}
|
||||
@@ -103,16 +128,19 @@ registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.newChatAction',
|
||||
title: 'View past chats',
|
||||
title: 'New Chat',
|
||||
icon: { id: 'add' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(IVoidSidebarStateService)
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
||||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
|
||||
const historyService = accessor.get(IThreadHistoryService)
|
||||
historyService.startNewThread()
|
||||
}
|
||||
@@ -123,31 +151,35 @@ registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.historyAction',
|
||||
title: 'View past chats',
|
||||
title: 'View Past Chats',
|
||||
icon: { id: 'history' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(IVoidSidebarStateService)
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
metricsService.capture('Chat Navigation', { type: 'History' })
|
||||
|
||||
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
|
||||
stateService.fireBlurChat()
|
||||
}
|
||||
})
|
||||
|
||||
// Settings (API config) menu button
|
||||
|
||||
// Settings gear
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.viewSettings',
|
||||
title: 'Void settings',
|
||||
id: 'void.settingsAction',
|
||||
title: 'Void Settings',
|
||||
icon: { id: 'settings-gear' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(IVoidSidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: stateService.state.currentTab === 'settings' ? 'chat' : 'settings' })
|
||||
stateService.fireBlurChat()
|
||||
const commandService = accessor.get(ICommandService)
|
||||
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,174 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import {
|
||||
Extensions as ViewContainerExtensions, IViewContainersRegistry,
|
||||
ViewContainerLocation, IViewsRegistry, Extensions as ViewExtensions,
|
||||
IViewDescriptorService,
|
||||
} from '../../../common/views.js';
|
||||
|
||||
import * as nls from '../../../../nls.js';
|
||||
|
||||
// import { Codicon } from '../../../../base/common/codicons.js';
|
||||
// import { localize } from '../../../../nls.js';
|
||||
// import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
|
||||
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
|
||||
|
||||
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
|
||||
// import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
|
||||
|
||||
import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js';
|
||||
|
||||
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
// import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
|
||||
import { mountSidebar } from './react/out/sidebar-tsx/index.js';
|
||||
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
|
||||
// import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
|
||||
// compare against search.contribution.ts and debug.contribution.ts, scm.contribution.ts (source control)
|
||||
|
||||
// ---------- Define viewpane ----------
|
||||
|
||||
class SidebarViewPane extends ViewPane {
|
||||
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
// @ICodeEditorService private readonly editorService: ICodeEditorService,
|
||||
// @IContextKeyService private readonly editorContextKeyService: IContextKeyService,
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override renderBody(parent: HTMLElement): void {
|
||||
super.renderBody(parent);
|
||||
// parent.style.overflow = 'auto'
|
||||
parent.style.userSelect = 'text'
|
||||
|
||||
// gets set immediately
|
||||
this.instantiationService.invokeFunction(accessor => {
|
||||
// mount react
|
||||
const disposables: IDisposable[] | undefined = mountSidebar(parent, accessor);
|
||||
disposables?.forEach(d => this._register(d))
|
||||
});
|
||||
}
|
||||
|
||||
protected override layoutBody(height: number, width: number): void {
|
||||
super.layoutBody(height, width)
|
||||
this.element.style.height = `${height}px`
|
||||
this.element.style.width = `${width}px`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------- Register viewpane inside the void container ----------
|
||||
|
||||
// const voidThemeIcon = Codicon.symbolObject;
|
||||
// const voidViewIcon = registerIcon('void-view-icon', voidThemeIcon, localize('voidViewIcon', 'View icon of the Void chat view.'));
|
||||
|
||||
// called VIEWLET_ID in other places for some reason
|
||||
export const VOID_VIEW_CONTAINER_ID = 'workbench.view.void'
|
||||
export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID
|
||||
|
||||
// Register view container
|
||||
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
const container = viewContainerRegistry.registerViewContainer({
|
||||
id: VOID_VIEW_CONTAINER_ID,
|
||||
title: nls.localize2('voidContainer', 'Void Chat'), // this is used to say "Void" (Ctrl + L)
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, {
|
||||
mergeViewWithContainerWhenSingleView: true,
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
}]),
|
||||
hideIfEmpty: false,
|
||||
order: 1,
|
||||
|
||||
rejectAddedViews: true,
|
||||
icon: Codicon.symbolMethod,
|
||||
|
||||
|
||||
}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true, isDefault: true });
|
||||
|
||||
|
||||
|
||||
// Register search default location to the container (sidebar)
|
||||
const viewsRegistry = Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry);
|
||||
viewsRegistry.registerViews([{
|
||||
id: VOID_VIEW_ID,
|
||||
hideByDefault: false, // start open
|
||||
// containerIcon: voidViewIcon,
|
||||
name: nls.localize2('voidChat', ''), // this says ... : CHAT
|
||||
ctorDescriptor: new SyncDescriptor(SidebarViewPane),
|
||||
canToggleVisibility: false,
|
||||
canMoveView: false, // can't move this out of its container
|
||||
weight: 80,
|
||||
order: 1,
|
||||
// singleViewPaneContainerTitle: 'hi',
|
||||
|
||||
// openCommandActionDescriptor: {
|
||||
// id: VOID_VIEW_CONTAINER_ID,
|
||||
// keybindings: {
|
||||
// primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
// },
|
||||
// order: 1
|
||||
// },
|
||||
}], container);
|
||||
|
||||
|
||||
// open sidebar
|
||||
export const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.openSidebar'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_OPEN_SIDEBAR_ACTION_ID,
|
||||
title: 'Open Void Sidebar',
|
||||
})
|
||||
}
|
||||
run(accessor: ServicesAccessor): void {
|
||||
const viewsService = accessor.get(IViewsService)
|
||||
viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
}
|
||||
});
|
||||
|
||||
export class SidebarStartContribution implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.startupVoidSidebar';
|
||||
constructor(
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
}
|
||||
registerWorkbenchContribution2(SidebarStartContribution.ID, SidebarStartContribution, WorkbenchPhase.AfterRestored);
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
|
||||
|
||||
|
||||
// service that manages sidebar's state
|
||||
export type VoidSidebarState = {
|
||||
isHistoryOpen: boolean;
|
||||
currentTab: 'chat';
|
||||
}
|
||||
|
||||
export interface ISidebarStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: VoidSidebarState; // readonly to the user
|
||||
setState(newState: Partial<VoidSidebarState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
onDidFocusChat: Event<void>;
|
||||
onDidBlurChat: Event<void>;
|
||||
fireFocusChat(): void;
|
||||
fireBlurChat(): void;
|
||||
}
|
||||
|
||||
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
|
||||
class VoidSidebarStateService extends Disposable implements ISidebarStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidSidebarStateService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onFocusChat = new Emitter<void>();
|
||||
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
|
||||
|
||||
private readonly _onBlurChat = new Emitter<void>();
|
||||
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
|
||||
|
||||
|
||||
// state
|
||||
state: VoidSidebarState
|
||||
|
||||
constructor(
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
this.state = { isHistoryOpen: false, currentTab: 'chat', }
|
||||
}
|
||||
|
||||
|
||||
setState(newState: Partial<VoidSidebarState>) {
|
||||
// make sure view is open if the tab changes
|
||||
if ('currentTab' in newState) {
|
||||
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
|
||||
+28
-11
@@ -1,6 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
@@ -10,30 +10,42 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IAutocompleteService } from './autocompleteService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
|
||||
// if selectionStr is null, it means just send the whole file
|
||||
export type CodeSelection = {
|
||||
selectionStr: string | null;
|
||||
fileURI: URI;
|
||||
content: string;
|
||||
selectionStr: string | null;
|
||||
content: string; // TODO remove this (replace `selectionStr` with `content`)
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
// if selectionStr is null, it means to use the entire file at send time
|
||||
export type CodeStagingSelection = {
|
||||
selectionStr: string | null;
|
||||
fileURI: URI;
|
||||
type: 'Selection',
|
||||
fileURI: URI,
|
||||
selectionStr: string,
|
||||
range: IRange
|
||||
} | {
|
||||
type: 'File',
|
||||
fileURI: URI,
|
||||
selectionStr: null,
|
||||
range: null
|
||||
}
|
||||
|
||||
|
||||
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string; // content sent to the llm
|
||||
displayContent: string; // content displayed to user
|
||||
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
|
||||
selections: CodeSelection[] | null; // the user's selection
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
content: string; // content received from LLM
|
||||
displayContent: string | undefined; // content displayed to user (this is the same as content for now)
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
@@ -68,7 +80,7 @@ const newThreadObject = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.threadsHistory'
|
||||
const THREAD_STORAGE_KEY = 'void.threadHistory'
|
||||
|
||||
export interface IThreadHistoryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
@@ -97,8 +109,10 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService {
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IAutocompleteService private readonly _autocomplete: IAutocompleteService,
|
||||
) {
|
||||
super()
|
||||
this._autocomplete
|
||||
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
@@ -133,6 +147,8 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService {
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
console.log('threadId', threadId)
|
||||
console.log('messages', this.state.allThreads[threadId].messages)
|
||||
this._setState({ _currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
@@ -194,3 +210,4 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService {
|
||||
}
|
||||
|
||||
registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// register keybinds
|
||||
import './registerActions.js'
|
||||
|
||||
// register Settings
|
||||
import './registerConfig.js'
|
||||
|
||||
// register inline diffs
|
||||
import './registerInlineDiffs.js'
|
||||
import './inlineDiffsService.js'
|
||||
|
||||
// register Posthog metrics
|
||||
import './registerMetrics.js'
|
||||
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
|
||||
import './sidebarActions.js'
|
||||
import './sidebarPane.js'
|
||||
import './sidebarStateService.js'
|
||||
|
||||
// register Sidebar chat
|
||||
import './registerSidebar.js'
|
||||
// register quick edit (Ctrl+K)
|
||||
import './quickEditActions.js'
|
||||
|
||||
// register Thread History
|
||||
import './registerThreads.js'
|
||||
import './threadHistoryService.js'
|
||||
|
||||
// register Autocomplete
|
||||
import './autocompleteService.js'
|
||||
|
||||
// settings pane
|
||||
import './voidSettingsPane.js'
|
||||
|
||||
// register css
|
||||
import './media/void.css'
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { EditorInput } from '../../../common/editor/editorInput.js';
|
||||
import * as nls from '../../../../nls.js';
|
||||
import { EditorExtensions } from '../../../common/editor.js';
|
||||
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
|
||||
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IStorageService } from '../../../../platform/storage/common/storage.js';
|
||||
import { Dimension } from '../../../../base/browser/dom.js';
|
||||
import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js';
|
||||
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
|
||||
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
|
||||
|
||||
import { mountVoidSettings } from './react/out/void-settings-tsx/index.js'
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
|
||||
|
||||
|
||||
// refer to preferences.contribution.ts keybindings editor
|
||||
|
||||
class VoidSettingsInput extends EditorInput {
|
||||
|
||||
static readonly ID: string = 'workbench.input.void.settings';
|
||||
|
||||
static readonly RESOURCE = URI.from({ // I think this scheme is invalid, it just shuts up TS
|
||||
scheme: 'void', // Custom scheme for our editor
|
||||
path: 'settings'
|
||||
})
|
||||
readonly resource = VoidSettingsInput.RESOURCE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override get typeId(): string {
|
||||
return VoidSettingsInput.ID;
|
||||
}
|
||||
|
||||
override getName(): string {
|
||||
return nls.localize('voidSettingsInputsName', 'Void Settings');
|
||||
}
|
||||
|
||||
override getIcon() {
|
||||
return Codicon.checklist // symbol for the actual editor pane
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class VoidSettingsPane extends EditorPane {
|
||||
static readonly ID = 'workbench.test.myCustomPane';
|
||||
|
||||
private _scrollbar: DomScrollableElement | undefined;
|
||||
|
||||
constructor(
|
||||
group: IEditorGroup,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService
|
||||
) {
|
||||
super(VoidSettingsPane.ID, group, telemetryService, themeService, storageService);
|
||||
}
|
||||
|
||||
protected createEditor(parent: HTMLElement): void {
|
||||
parent.style.height = '100%';
|
||||
parent.style.width = '100%';
|
||||
|
||||
const scrollableContent = document.createElement('div');
|
||||
scrollableContent.style.height = '100%';
|
||||
scrollableContent.style.width = '100%';
|
||||
|
||||
this._scrollbar = this._register(new DomScrollableElement(scrollableContent, {}));
|
||||
parent.appendChild(this._scrollbar.getDomNode());
|
||||
this._scrollbar.scanDomNode();
|
||||
|
||||
// Mount React into the scrollable content
|
||||
this.instantiationService.invokeFunction(accessor => {
|
||||
const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, accessor);
|
||||
|
||||
setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
|
||||
this._scrollbar?.scanDomNode();
|
||||
}, 1000)
|
||||
disposables?.forEach(d => this._register(d));
|
||||
});
|
||||
}
|
||||
|
||||
layout(dimension: Dimension): void {
|
||||
if (!this._scrollbar) return;
|
||||
|
||||
this._scrollbar.getDomNode().style.height = `${dimension.height}px`;
|
||||
this._scrollbar.getDomNode().style.width = `${dimension.width}px`;
|
||||
this._scrollbar.scanDomNode();
|
||||
|
||||
}
|
||||
|
||||
|
||||
override get minimumWidth() { return 700 }
|
||||
|
||||
}
|
||||
|
||||
// register Settings pane
|
||||
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
|
||||
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void Settings Pane")),
|
||||
[new SyncDescriptor(VoidSettingsInput)]
|
||||
);
|
||||
|
||||
|
||||
export const VOID_OPEN_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
|
||||
// register the gear on the top right
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
title: nls.localize2('voidSettings', "Void: Settings"),
|
||||
f1: true,
|
||||
icon: Codicon.settingsGear,
|
||||
menu: [
|
||||
{
|
||||
id: MenuId.LayoutControlMenuSubmenu,
|
||||
group: 'z_end',
|
||||
},
|
||||
{
|
||||
id: MenuId.LayoutControlMenu,
|
||||
when: ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both'),
|
||||
group: 'z_end'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
|
||||
|
||||
// close all instances if found
|
||||
if (openEditors.length > 0) {
|
||||
await editorService.closeEditors(openEditors);
|
||||
return;
|
||||
}
|
||||
|
||||
// else open it
|
||||
const input = instantiationService.createInstance(VoidSettingsInput);
|
||||
await editorService.openEditor(input);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// add to settings gear on bottom left
|
||||
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
|
||||
group: '0_command',
|
||||
command: {
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
title: nls.localize('voidSettings', "Void Settings")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
@@ -817,7 +817,7 @@ export class GettingStartedPage extends EditorPane {
|
||||
|
||||
const header = $('.header', {},
|
||||
$('h1.product-name.caption', {}, this.productService.nameLong),
|
||||
$('p.subtitle.description', {}, localize({ key: 'gettingStarted.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "Editing evolved"))
|
||||
$('p.subtitle.description', {}, localize({ key: 'gettingStarted.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "The open source AI code editor."))
|
||||
);
|
||||
|
||||
const leftColumn = $('.categories-column.categories-column-left', {},);
|
||||
|
||||
@@ -248,30 +248,30 @@ export const walkthroughs: GettingStartedWalkthroughContent = [
|
||||
type: 'svg', altText: 'Language extensions', path: 'languages.svg'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
title: localize('gettingStarted.settings.title', "Tune your settings"),
|
||||
description: localize('gettingStarted.settings.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')),
|
||||
media: {
|
||||
type: 'svg', altText: 'VS Code Settings', path: 'settings.svg'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'settingsSync',
|
||||
title: localize('gettingStarted.settingsSync.title', "Sync settings across devices"),
|
||||
description: localize('gettingStarted.settingsSync.description.interpolated', "Keep your essential customizations backed up and updated across all your devices.\n{0}", Button(localize('enableSync', "Backup and Sync Settings"), 'command:workbench.userDataSync.actions.turnOn')),
|
||||
when: 'syncStatus != uninitialized',
|
||||
completionEvents: ['onEvent:sync-enabled'],
|
||||
media: {
|
||||
type: 'svg', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: 'settingsSync.svg'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'commandPaletteTask',
|
||||
title: localize('gettingStarted.commandPalette.title', "Unlock productivity with the Command Palette "),
|
||||
description: localize('gettingStarted.commandPalette.description.interpolated', "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')),
|
||||
media: { type: 'svg', altText: 'Command Palette overlay for searching and executing commands.', path: 'commandPalette.svg' },
|
||||
},
|
||||
// {
|
||||
// id: 'settings',
|
||||
// title: localize('gettingStarted.settings.title', "Tune your settings"),
|
||||
// description: localize('gettingStarted.settings.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')),
|
||||
// media: {
|
||||
// type: 'svg', altText: 'VS Code Settings', path: 'settings.svg'
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// id: 'settingsSync',
|
||||
// title: localize('gettingStarted.settingsSync.title', "Sync settings across devices"),
|
||||
// description: localize('gettingStarted.settingsSync.description.interpolated', "Keep your essential customizations backed up and updated across all your devices.\n{0}", Button(localize('enableSync', "Backup and Sync Settings"), 'command:workbench.userDataSync.actions.turnOn')),
|
||||
// when: 'syncStatus != uninitialized',
|
||||
// completionEvents: ['onEvent:sync-enabled'],
|
||||
// media: {
|
||||
// type: 'svg', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: 'settingsSync.svg'
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// id: 'commandPaletteTask',
|
||||
// title: localize('gettingStarted.commandPalette.title', "Unlock productivity with the Command Palette "),
|
||||
// description: localize('gettingStarted.commandPalette.description.interpolated', "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')),
|
||||
// media: { type: 'svg', altText: 'Command Palette overlay for searching and executing commands.', path: 'commandPalette.svg' },
|
||||
// },
|
||||
{
|
||||
id: 'pickAFolderTask-Mac',
|
||||
title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"),
|
||||
@@ -299,12 +299,12 @@ export const walkthroughs: GettingStartedWalkthroughContent = [
|
||||
type: 'svg', altText: 'Go to file in quick search.', path: 'search.svg'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'videoTutorial',
|
||||
title: localize('gettingStarted.videoTutorial.title', "Watch video tutorials"),
|
||||
description: localize('gettingStarted.videoTutorial.description.interpolated', "Watch the first in a series of short & practical video tutorials for VS Code's key features.\n{0}", Button(localize('watch', "Watch Tutorial"), 'https://aka.ms/vscode-getting-started-video')),
|
||||
media: { type: 'svg', altText: 'VS Code Settings', path: 'learn.svg' },
|
||||
}
|
||||
// {
|
||||
// id: 'videoTutorial',
|
||||
// title: localize('gettingStarted.videoTutorial.title', "Watch video tutorials"),
|
||||
// description: localize('gettingStarted.videoTutorial.description.interpolated', "Watch the first in a series of short & practical video tutorials for VS Code's key features.\n{0}", Button(localize('watch', "Watch Tutorial"), 'https://aka.ms/vscode-getting-started-video')),
|
||||
// media: { type: 'svg', altText: 'VS Code Settings', path: 'learn.svg' },
|
||||
// }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import './browser/workbench.contribution.js';
|
||||
//#region --- Void
|
||||
// Void added this:
|
||||
import './contrib/void/browser/void.contribution.js';
|
||||
import '../platform/void/browser/llmMessageService.js';
|
||||
import '../platform/void/browser/void.contribution.js';
|
||||
//#endregion
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ import './contrib/surveys/browser/nps.contribution.js';
|
||||
import './contrib/surveys/browser/languageSurveys.contribution.js';
|
||||
|
||||
// Welcome
|
||||
import './contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js';
|
||||
// import './contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js'; // Void commented this out (removes Welcome page on start)
|
||||
import './contrib/welcomeWalkthrough/browser/walkThrough.contribution.js';
|
||||
import './contrib/welcomeViews/common/viewsWelcome.contribution.js';
|
||||
import './contrib/welcomeViews/common/newFile.contribution.js';
|
||||
|
||||
@@ -31,12 +31,6 @@ import './electron-sandbox/parts/dialogs/dialog.contribution.js';
|
||||
|
||||
//#endregion
|
||||
|
||||
// //#region --- Void
|
||||
// // Void added this (modeling off of import '.*clipboardservice.js'):
|
||||
// import './services/void/electron-main/sendLLMMessage.js';
|
||||
// //#endregion
|
||||
|
||||
|
||||
|
||||
//#region --- workbench services
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user