Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac8365def4 | |||
| 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",
|
||||
|
||||
+51
-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 buildreact` 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+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building, unless they're React changes.
|
||||
|
||||
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,50 @@ 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`.
|
||||
- If you make any React changes, you must re-run `npm run buildreact` and 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, 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 +135,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
+195
-135
@@ -10,9 +10,13 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"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",
|
||||
@@ -35,6 +39,8 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.64",
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"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",
|
||||
@@ -45,7 +51,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",
|
||||
@@ -55,13 +67,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",
|
||||
@@ -102,7 +113,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",
|
||||
@@ -146,8 +156,6 @@
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"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",
|
||||
@@ -155,14 +163,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",
|
||||
@@ -216,7 +220,6 @@
|
||||
"version": "0.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz",
|
||||
"integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
@@ -232,7 +235,6 @@
|
||||
"version": "18.19.64",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
|
||||
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
@@ -1000,7 +1002,6 @@
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
|
||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
@@ -1681,7 +1682,6 @@
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
|
||||
"integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -2598,6 +2598,31 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rrweb/record": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/@rrweb/record/-/record-2.0.0-alpha.17.tgz",
|
||||
"integrity": "sha512-Je+lzjeWMF8/I0IDoXFzkGPKT8j7AkaBup5YcwUHlkp18VhLVze416MvI6915teE27uUA2ScXMXzG0Yiu5VTIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rrweb/types": "^2.0.0-alpha.17",
|
||||
"rrweb": "^2.0.0-alpha.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@rrweb/types": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.17.tgz",
|
||||
"integrity": "sha512-AfDTVUuCyCaIG0lTSqYtrZqJX39ZEYzs4fYKnexhQ+id+kbZIpIJtaut5cto6dWZbB3SEe4fW0o90Po3LvTmfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rrweb-snapshot": "^2.0.0-alpha.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@rrweb/utils": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/@rrweb/utils/-/utils-2.0.0-alpha.17.tgz",
|
||||
"integrity": "sha512-HCsasPERBwOS9/LQeOytO2ETKTCqRj1wORBuxiy3t41hKhmi225DdrUPiWnyDdTQm1GdVbOymMRknJVPnZaSXw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
@@ -2971,6 +2996,12 @@
|
||||
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/css-font-loading-module": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz",
|
||||
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz",
|
||||
@@ -2987,6 +3018,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
@@ -3026,7 +3068,6 @@
|
||||
"version": "2.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
|
||||
"integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2"
|
||||
@@ -3103,7 +3144,6 @@
|
||||
"version": "20.14.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz",
|
||||
"integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
@@ -3112,7 +3152,6 @@
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
|
||||
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -3215,7 +3254,6 @@
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/vinyl": {
|
||||
@@ -4424,6 +4462,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@xstate/fsm": {
|
||||
"version": "1.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz",
|
||||
"integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/addon-clipboard": {
|
||||
"version": "0.2.0-beta.47",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.47.tgz",
|
||||
@@ -4507,7 +4551,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
@@ -4588,7 +4631,6 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
|
||||
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
@@ -5143,8 +5185,7 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
@@ -5212,6 +5253,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
|
||||
@@ -5320,6 +5372,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -5795,7 +5856,6 @@
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
|
||||
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5806,7 +5866,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
|
||||
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5817,7 +5876,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
|
||||
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -6288,7 +6346,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -6300,7 +6357,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -6309,7 +6365,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
|
||||
"integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -6578,18 +6633,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.39.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
|
||||
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
@@ -7207,7 +7250,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
@@ -8070,7 +8112,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -8457,7 +8498,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
||||
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"format": "^0.2.0"
|
||||
@@ -8490,13 +8530,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.4.8",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
|
||||
@@ -8899,6 +8932,26 @@
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
@@ -8949,7 +9002,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -8963,14 +9015,12 @@
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
@@ -8979,7 +9029,6 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
@@ -9893,6 +9942,30 @@
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/groq-sdk": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.9.0.tgz",
|
||||
"integrity": "sha512-Kdbl65yOwd5ga+UZd3KCULnoOlcPV7qJ+FfemivVpu3bOZeT7WJWh/ywAuivwJ7aJzcXmwf6x5t9x0MV0ZmR0A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/groq-sdk/node_modules/@types/node": {
|
||||
"version": "18.19.67",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz",
|
||||
"integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gulp": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
|
||||
@@ -11955,7 +12028,6 @@
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
|
||||
"integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -11966,7 +12038,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
|
||||
"integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^2.0.0",
|
||||
@@ -11993,7 +12064,6 @@
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -12003,7 +12073,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
|
||||
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/homedir-polyfill": {
|
||||
@@ -12140,7 +12209,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
@@ -12650,7 +12718,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
|
||||
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -12661,7 +12728,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
|
||||
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-alphabetical": "^1.0.0",
|
||||
@@ -12873,7 +12939,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
|
||||
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -13000,7 +13065,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
|
||||
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -13538,8 +13602,7 @@
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
@@ -14219,7 +14282,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
@@ -14241,7 +14303,6 @@
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
|
||||
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fault": "^1.0.0",
|
||||
@@ -14715,7 +14776,6 @@
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
|
||||
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -14724,7 +14784,6 @@
|
||||
"version": "2.1.28",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
|
||||
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.45.0"
|
||||
},
|
||||
@@ -14806,6 +14865,12 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||
@@ -15339,7 +15404,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -15359,7 +15423,6 @@
|
||||
"version": "2.6.8",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
|
||||
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
@@ -15909,10 +15972,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ollama": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz",
|
||||
"integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==",
|
||||
"dev": true,
|
||||
"version": "0.5.11",
|
||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.11.tgz",
|
||||
"integrity": "sha512-lDAKcpmBU3VAOGF05NcQipHNKTdpKfAHpZ7bjCsElkUkmX7SNZImi6lwIxz/l1zQtLq0S3wuLneRuiXxX2KIew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
@@ -15985,10 +16047,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.71.1",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.71.1.tgz",
|
||||
"integrity": "sha512-C6JNMaQ1eijM0lrjiRUL3MgThVP5RdwNAghpbJFdW0t11LzmyqON8Eh8MuUuEZ+CeD6bgYl2Fkn2BoptVxv9Ug==",
|
||||
"dev": true,
|
||||
"version": "4.76.1",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.76.1.tgz",
|
||||
"integrity": "sha512-ci63/WFEMd6QjjEVeH0pV7hnFS6CCqhgJydSti4Aak/8uo2SpgzKjteUDaY+OkwziVj11mi6j+0mRUIiGKUzWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
@@ -16015,7 +16076,6 @@
|
||||
"version": "18.19.64",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
|
||||
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
@@ -16325,7 +16385,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
|
||||
"integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities": "^1.0.0",
|
||||
@@ -16570,7 +16629,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -16816,7 +16874,6 @@
|
||||
"version": "8.4.48",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.48.tgz",
|
||||
"integrity": "sha512-GCRK8F6+Dl7xYniR5a4FYbpBzU8XnZVeowqsQFYdcXuSbChgiks7qybSkbvnaeqv0G0B+dd9/jJgH8kkLDQeEA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -17550,7 +17607,6 @@
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -17564,28 +17620,17 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.184.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.184.2.tgz",
|
||||
"integrity": "sha512-n7OxFntH7m4sw/GsuUAliWGqBDV7g7IemvjwGISCQFQpf5cSqZgmvT2V2O3GHXpONiEgwL/Ud/+zWmhZbWLExg==",
|
||||
"dev": true,
|
||||
"node_modules/posthog-node": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.3.1.tgz",
|
||||
"integrity": "sha512-By9SEGZxBLC7GVyVb+HlJlpxM/xI4iLUgwtsBS8f4bZ0wqYKiNHoYcFEwMJnTM9xQcQZztr6dqLmsJ7jCv0vxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.24.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
"axios": "^1.7.4",
|
||||
"rusha": "^0.8.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
@@ -17675,7 +17720,6 @@
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -17709,7 +17753,6 @@
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
|
||||
"integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
@@ -17900,7 +17943,6 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
@@ -17913,7 +17955,6 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
@@ -17927,7 +17968,6 @@
|
||||
"version": "15.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
|
||||
"integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
@@ -18145,7 +18185,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
|
||||
"integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hastscript": "^6.0.0",
|
||||
@@ -18161,7 +18200,6 @@
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
|
||||
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -18171,7 +18209,6 @@
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regex-not": {
|
||||
@@ -18667,6 +18704,40 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rrdom": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.0.0-alpha.17.tgz",
|
||||
"integrity": "sha512-b6caDiNcFO96Opp7TGdcVd4OLGSXu5dJe+A0IDiAu8mk7OmhqZCSDlgQdTKmdO5wMf4zPsUTgb8H/aNvR3kDHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rrweb-snapshot": "^2.0.0-alpha.17"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.0.0-alpha.17.tgz",
|
||||
"integrity": "sha512-GQxBkCC4r9XL2bwSdv7iIS49M3cEA8OtObVq0rrQ4GUT4+h7omucGQ4x7m5YN5Vq1oalStBaBlYqF7yRnfG3JA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rrweb/types": "^2.0.0-alpha.17",
|
||||
"@rrweb/utils": "^2.0.0-alpha.17",
|
||||
"@types/css-font-loading-module": "0.0.7",
|
||||
"@xstate/fsm": "^1.4.0",
|
||||
"base64-arraybuffer": "^1.0.1",
|
||||
"mitt": "^3.0.0",
|
||||
"rrdom": "^2.0.0-alpha.17",
|
||||
"rrweb-snapshot": "^2.0.0-alpha.17"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-snapshot": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.17.tgz",
|
||||
"integrity": "sha512-GBg5pV8LHOTbeVmH2VHLEFR0mc2QpQMzAvcoxEGfPNWgWHc8UvKCyq7pqN1vA+fDZ+yXXbixeO0kB2pzVvFCBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss": "^8.4.38"
|
||||
}
|
||||
},
|
||||
"node_modules/run-async": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||
@@ -18696,6 +18767,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/rusha": {
|
||||
"version": "0.8.14",
|
||||
"resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz",
|
||||
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
|
||||
@@ -18788,7 +18865,6 @@
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
@@ -18848,9 +18924,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/scope-tailwind": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.1.tgz",
|
||||
"integrity": "sha512-u65hZidTP5LhJL2jufMQwi9ulqdJ6NGx3gYKDwuTYQJz/oaYpOs76cLHCp4ul79KVr7lPWRHTlSqLfs55wrqyA==",
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.5.tgz",
|
||||
"integrity": "sha512-h7WhPy9LocCjtG0r2ijLJmVa0huJlk8wrlCSEOV08qowsNgFScVMqY8ocIbIfnZvgcOyZlUWtCjO+sf6Q0T9nA==",
|
||||
"dev": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
@@ -19594,7 +19670,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -19643,7 +19718,6 @@
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
|
||||
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -21199,8 +21273,7 @@
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
@@ -21794,8 +21867,7 @@
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/union-value": {
|
||||
"version": "1.0.1",
|
||||
@@ -22269,7 +22341,6 @@
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -22281,18 +22352,10 @@
|
||||
"integrity": "sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/web-vitals": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
|
||||
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true
|
||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.94.0",
|
||||
@@ -22553,14 +22616,12 @@
|
||||
"version": "3.6.20",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
@@ -22788,7 +22849,6 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
|
||||
+15
-10
@@ -9,6 +9,7 @@
|
||||
"main": "./out/main",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && 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 +73,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 +102,8 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.64",
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"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 +114,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 +130,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 +176,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",
|
||||
@@ -208,8 +219,6 @@
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"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 +226,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",
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
"nodejsRepository": "https://nodejs.org",
|
||||
"urlProtocol": "code-oss",
|
||||
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://open-vsx.org/vscode/gallery",
|
||||
"itemUrl": "https://open-vsx.org/vscode/item"
|
||||
},
|
||||
"builtInExtensions": [
|
||||
{
|
||||
"name": "ms-vscode.js-debug-companion",
|
||||
|
||||
+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));
|
||||
|
||||
@@ -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,146 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ServiceOllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams, MainOllamaListParams } 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: ServiceOllamaListParams) => 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: EventOllamaListOnSuccessParams) => void) } = {}
|
||||
private readonly onError_ollama: { [eventId: string]: ((params: EventOllamaListOnErrorParams) => 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<EventOllamaListOnSuccessParams>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventOllamaListOnErrorParams>)(e => {
|
||||
this.onError_ollama[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: ServiceOllamaListParams) => {
|
||||
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 MainOllamaListParams)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_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,124 @@ export type LLMMessage = {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type LLMMessageServiceParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
voidConfig: VoidConfig | null;
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
export type LLMFeatureSelection = {
|
||||
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,
|
||||
};
|
||||
} & LLMFeatureSelection
|
||||
|
||||
// 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 ModelDetails {
|
||||
parent_model: string;
|
||||
format: string;
|
||||
family: string;
|
||||
families: string[];
|
||||
parameter_size: string;
|
||||
quantization_level: string;
|
||||
}
|
||||
|
||||
export type ModelResponse = {
|
||||
name: string;
|
||||
modified_at: Date;
|
||||
size: number;
|
||||
digest: string;
|
||||
details: ModelDetails;
|
||||
expires_at: Date;
|
||||
size_vram: number;
|
||||
}
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
export type OllamaListParams = {
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
onSuccess: (param: { models: ModelResponse[] }) => void;
|
||||
onError: (param: { error: string }) => void;
|
||||
}
|
||||
|
||||
export type ServiceOllamaListParams = {
|
||||
onSuccess: (param: { models: ModelResponse[] }) => void;
|
||||
onError: (param: { error: any }) => void;
|
||||
}
|
||||
|
||||
type BlockedMainOllamaListParams = 'onSuccess' | 'onError'
|
||||
export type MainOllamaListParams = Omit<OllamaListParams, BlockedMainOllamaListParams> & { requestId: string }
|
||||
|
||||
export type EventOllamaListOnSuccessParams = Parameters<OllamaListParams['onSuccess']>[0] & { requestId: string }
|
||||
export type EventOllamaListOnErrorParams = Parameters<OllamaListParams['onError']>[0] & { requestId: string }
|
||||
|
||||
|
||||
|
||||
export type _InternalOllamaListFnType = (params: OllamaListParams) => 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,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 } from '../../../base/common/lifecycle.js';
|
||||
|
||||
|
||||
export type RefreshModelState = 'done' | 'loading'
|
||||
|
||||
// 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;
|
||||
refreshOllamaModels(): void;
|
||||
onDidChangeState: Event<void>;
|
||||
state: RefreshModelState;
|
||||
}
|
||||
|
||||
export const IRefreshModelService = createDecorator<IRefreshModelService>('RefreshModelService');
|
||||
|
||||
export class RefreshModelService extends Disposable implements IRefreshModelService {
|
||||
|
||||
readonly _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
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// on mount, refresh ollama models
|
||||
this.refreshOllamaModels()
|
||||
|
||||
// every time ollama.enabled changes, refresh ollama models, like useEffect
|
||||
let relevantVals = () => [this.voidSettingsService.state.settingsOfProvider.ollama.enabled, this.voidSettingsService.state.settingsOfProvider.ollama.endpoint]
|
||||
let prevVals = relevantVals()
|
||||
this._register(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
this.refreshOllamaModels()
|
||||
prevVals = newVals
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
state: RefreshModelState = 'done'
|
||||
|
||||
private _timeoutId: NodeJS.Timeout | null = null
|
||||
private _cancelTimeout = () => {
|
||||
if (this._timeoutId) {
|
||||
clearTimeout(this._timeoutId)
|
||||
this._timeoutId = null
|
||||
}
|
||||
}
|
||||
async refreshOllamaModels() {
|
||||
// cancel any existing poll
|
||||
this._cancelTimeout()
|
||||
|
||||
// if ollama is disabled, obivously done
|
||||
if (!this.voidSettingsService.state.settingsOfProvider.ollama.enabled) {
|
||||
this._setState('done')
|
||||
return
|
||||
}
|
||||
|
||||
// start loading models
|
||||
this._setState('loading')
|
||||
|
||||
this.llmMessageService.ollamaList({
|
||||
onSuccess: ({ models }) => {
|
||||
this.voidSettingsService.setDefaultModels('ollama', models.map(model => model.name))
|
||||
this._setState('done')
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
// poll
|
||||
console.log('retrying ollamaList:', error)
|
||||
this._timeoutId = setTimeout(() => this.refreshOllamaModels(), 5000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private _setState(state: RefreshModelState) {
|
||||
this.state = state
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IRefreshModelService, RefreshModelService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ModelInfo } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.voidSettingsI'
|
||||
|
||||
type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
providerName: ProviderName,
|
||||
settingName: S,
|
||||
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
|
||||
) => Promise<void>;
|
||||
|
||||
type SetModelSelectionOfFeature = <K extends FeatureName>(
|
||||
featureName: K,
|
||||
newVal: ModelSelectionOfFeature[K],
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
|
||||
|
||||
export type ModelOption = { text: string, value: ModelSelection }
|
||||
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: VoidSettingsState;
|
||||
onDidChangeState: Event<void>;
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeature;
|
||||
|
||||
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 },
|
||||
_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<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
|
||||
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()
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(s => {
|
||||
this.state = s
|
||||
this._onDidChangeState.fire()
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
_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()
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeature = 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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
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: ModelInfo[] = [
|
||||
...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,308 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
|
||||
export type ModelInfo = {
|
||||
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[]): ModelInfo[] => {
|
||||
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 customProviderSettingsDefaults = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAI: {
|
||||
apiKey: '',
|
||||
},
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
openRouter: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAICompatible: {
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
},
|
||||
gemini: {
|
||||
apiKey: '',
|
||||
},
|
||||
groq: {
|
||||
apiKey: ''
|
||||
}
|
||||
} as const
|
||||
|
||||
export type ProviderName = keyof typeof customProviderSettingsDefaults
|
||||
export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[]
|
||||
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof customProviderSettingsDefaults[ProviderName]>
|
||||
|
||||
type CustomProviderSettings<providerName extends ProviderName> = {
|
||||
[k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[providerName] ? string : undefined
|
||||
}
|
||||
|
||||
type CommonProviderSettings = {
|
||||
enabled: boolean,
|
||||
models: ModelInfo[], // if null, user can type in any string as a model
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
|
||||
|
||||
export const titleOfProviderName = (providerName: ProviderName) => {
|
||||
if (providerName === 'anthropic')
|
||||
return 'Anthropic'
|
||||
else if (providerName === 'openAI')
|
||||
return 'OpenAI'
|
||||
else if (providerName === 'ollama')
|
||||
return 'Ollama'
|
||||
else if (providerName === 'openRouter')
|
||||
return 'OpenRouter'
|
||||
else if (providerName === 'openAICompatible')
|
||||
return 'OpenAI-Compatible'
|
||||
else if (providerName === 'gemini')
|
||||
return 'Gemini'
|
||||
else if (providerName === 'groq')
|
||||
return 'Groq'
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
||||
type DisplayInfo = {
|
||||
title: string,
|
||||
placeholder: 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)',
|
||||
}
|
||||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Your Ollama endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
|
||||
: '(never)',
|
||||
placeholder: providerName === 'ollama' ? customProviderSettingsDefaults.ollama.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
}
|
||||
}
|
||||
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: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
enabled: false,
|
||||
},
|
||||
openAI: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
enabled: false,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
enabled: false,
|
||||
},
|
||||
groq: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
enabled: false,
|
||||
},
|
||||
ollama: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
enabled: false,
|
||||
},
|
||||
openRouter: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
enabled: false,
|
||||
},
|
||||
openAICompatible: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -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,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalOllamaListFnType, _InternalSendLLMMessageFnType, ModelResponse } from '../../common/llmMessageTypes.js';
|
||||
|
||||
export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
||||
const onSuccess = ({ models }: { models: ModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
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
|
||||
|
||||
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,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
// 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.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,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, MainOllamaListParams, OllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ollamaList } from './llmMessage/ollama.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<EventOllamaListOnSuccessParams>();
|
||||
private readonly _onError_ollama = new Emitter<EventOllamaListOnErrorParams>();
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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: MainOllamaListParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: OllamaListParams = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
|
||||
}
|
||||
ollamaList(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) })
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
+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';
|
||||
@@ -0,0 +1,51 @@
|
||||
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 { ISidebarStateService } from '../sidebarStateService.js';
|
||||
import { IThreadHistoryService } from '../threadHistoryService.js';
|
||||
|
||||
export type ReactServicesType = {
|
||||
sidebarStateService: ISidebarStateService;
|
||||
settingsStateService: IVoidSettingsService;
|
||||
threadsStateService: IThreadHistoryService;
|
||||
fileService: IFileService;
|
||||
modelService: IModelService;
|
||||
inlineDiffService: IInlineDiffsService;
|
||||
llmMessageService: ILLMMessageService;
|
||||
clipboardService: IClipboardService;
|
||||
refreshModelService: IRefreshModelService;
|
||||
|
||||
themeService: IThemeService,
|
||||
hoverService: IHoverService,
|
||||
|
||||
contextViewService: IContextViewService;
|
||||
contextMenuService: IContextMenuService;
|
||||
}
|
||||
|
||||
|
||||
export const getReactServices = (accessor: ServicesAccessor): ReactServicesType => {
|
||||
return {
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
threadsStateService: accessor.get(IThreadHistoryService),
|
||||
fileService: accessor.get(IFileService),
|
||||
modelService: accessor.get(IModelService),
|
||||
inlineDiffService: accessor.get(IInlineDiffsService),
|
||||
llmMessageService: accessor.get(ILLMMessageService),
|
||||
clipboardService: accessor.get(IClipboardService),
|
||||
themeService: accessor.get(IThemeService),
|
||||
hoverService: accessor.get(IHoverService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
contextViewService: accessor.get(IContextViewService),
|
||||
contextMenuService: accessor.get(IContextMenuService),
|
||||
}
|
||||
}
|
||||
|
||||
+17
-45
@@ -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';
|
||||
@@ -11,9 +11,8 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
|
||||
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
// import { throttle } from '../../../../base/common/decorators.js';
|
||||
import { IVoidConfigStateService } from './registerConfig.js';
|
||||
import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js';
|
||||
import { ComputedDiff, findDiffs } from './findDiffs.js';
|
||||
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
|
||||
import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
|
||||
@@ -28,10 +27,8 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
import { Widget } from '../../../../base/browser/ui/widget.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { LLMMessageServiceParams } from '../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js';
|
||||
// import { ISendLLMMessageService } from '../../../../platform/void/common/sendLLMMessage.js';
|
||||
// import { sendLLMMessage } from './react/out/util/sendLLMMessage.js';
|
||||
import { LLMFeatureSelection, ServiceSendLLMMessageParams } from '../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
|
||||
|
||||
|
||||
// gets converted to --vscode-void-greenBG, see void.css
|
||||
@@ -105,17 +102,17 @@ type HistorySnapshot = {
|
||||
entireFileCode: string;
|
||||
} &
|
||||
({
|
||||
type: 'ctrl+k';
|
||||
type: 'Ctrl+K';
|
||||
ctrlKText: string;
|
||||
} | {
|
||||
type: 'ctrl+l';
|
||||
type: 'Ctrl+L';
|
||||
})
|
||||
|
||||
|
||||
|
||||
export interface IInlineDiffsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
startStreaming(type: 'ctrl+k' | 'ctrl+l', userMessage: string): void;
|
||||
|
||||
startStreaming(params: LLMFeatureSelection, str: string): void;
|
||||
}
|
||||
|
||||
export const IInlineDiffsService = createDecorator<IInlineDiffsService>('inlineDiffAreasService');
|
||||
@@ -146,12 +143,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
|
||||
constructor(
|
||||
// @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
|
||||
@IVoidConfigStateService private readonly _voidConfigStateService: IVoidConfigStateService,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
|
||||
@ILanguageService private readonly _langService: ILanguageService,
|
||||
@ISendLLMMessageService private readonly _sendLLMMessageService: ISendLLMMessageService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -372,7 +368,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
return {
|
||||
snapshottedDiffAreaOfId,
|
||||
entireFileCode: this._readURI(uri) ?? '', // the whole file's code
|
||||
type: 'ctrl+l',
|
||||
type: 'Ctrl+L',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,7 +635,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
|
||||
|
||||
|
||||
private async _initializeStream(uri: URI, diffRepr: string) {
|
||||
private async _initializeStream(opts: LLMFeatureSelection, diffRepr: string, uri: URI,) {
|
||||
|
||||
// diff area begin and end line
|
||||
const numLines = this._getNumLines(uri)
|
||||
@@ -691,7 +687,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
this.diffAreaOfId[diffArea.diffareaid] = diffArea
|
||||
|
||||
// actually call the LLM
|
||||
const { voidConfig } = this._voidConfigStateService.state
|
||||
const promptContent = `\
|
||||
ORIGINAL_CODE
|
||||
\`\`\`
|
||||
@@ -708,34 +703,11 @@ Please finish writing the new file by applying the diff to the original file. Re
|
||||
`
|
||||
|
||||
|
||||
// CTRL+K prompt:
|
||||
// const promptContent = `Here is the user's original selection:
|
||||
// \`\`\`
|
||||
// <MID>${selection}</MID>
|
||||
// \`\`\`
|
||||
|
||||
// The user wants to apply the following instructions to the selection:
|
||||
// ${instructions}
|
||||
|
||||
// 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:
|
||||
// \`\`\`
|
||||
// <PRE>${prefix}</PRE>
|
||||
// <SUF>${suffix}</SUF>
|
||||
// <MID>`;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
|
||||
let streamRequestId: string | null = null
|
||||
|
||||
const object: LLMMessageServiceParams = {
|
||||
const object: ServiceSendLLMMessageParams = {
|
||||
logging: { loggingName: 'streamChunk' },
|
||||
messages: [
|
||||
{ role: 'system', content: writeFileWithDiffInstructions, },
|
||||
@@ -758,15 +730,15 @@ Please finish writing the new file by applying the diff to the original file. Re
|
||||
console.error('Error rewriting file with diff', e);
|
||||
// TODO indicate there was an error
|
||||
if (streamRequestId)
|
||||
this._sendLLMMessageService.abort(streamRequestId)
|
||||
this._llmMessageService.abort(streamRequestId)
|
||||
|
||||
diffArea._sweepState = { isStreaming: false, line: null }
|
||||
resolve();
|
||||
},
|
||||
voidConfig,
|
||||
...opts
|
||||
}
|
||||
|
||||
streamRequestId = this._sendLLMMessageService.sendLLMMessage(object)
|
||||
streamRequestId = this._llmMessageService.sendLLMMessage(object)
|
||||
})
|
||||
|
||||
onFinishEdit()
|
||||
@@ -778,7 +750,7 @@ Please finish writing the new file by applying the diff to the original file. Re
|
||||
|
||||
|
||||
|
||||
async startStreaming(type: 'ctrl+k' | 'ctrl+l', userMessage: string) {
|
||||
async startStreaming(opts: LLMFeatureSelection, userMessage: string) {
|
||||
|
||||
const editor = this._editorService.getActiveCodeEditor()
|
||||
if (!editor) return
|
||||
@@ -790,7 +762,7 @@ Please finish writing the new file by applying the diff to the original file. Re
|
||||
|
||||
// TODO deselect user's cursor
|
||||
|
||||
this._initializeStream(uri, userMessage)
|
||||
this._initializeStream(opts, userMessage, uri)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 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 userInstructionsStr = (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;
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// // used for ctrl+l
|
||||
// const partialGenerationInstructions = ``
|
||||
@@ -9,6 +11,116 @@
|
||||
// const fimInstructions = ``
|
||||
|
||||
|
||||
// CTRL+K prompt:
|
||||
// const promptContent = `Here is the user's original selection:
|
||||
// \`\`\`
|
||||
// <MID>${selection}</MID>
|
||||
// \`\`\`
|
||||
|
||||
// The user wants to apply the following instructions to the selection:
|
||||
// ${instructions}
|
||||
|
||||
// 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:
|
||||
// \`\`\`
|
||||
// <PRE>${prefix}</PRE>
|
||||
// <SUF>${suffix}</SUF>
|
||||
// <MID>`;
|
||||
|
||||
export const generateCtrlLInstructions = `\
|
||||
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
|
||||
`
|
||||
|
||||
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\`.
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// clear temp dirs
|
||||
execSync('npx rimraf out/ && npx rimraf src2/')
|
||||
|
||||
// build and scope tailwind
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "prefix-" ')
|
||||
// build and scope tailwind: https://www.npmjs.com/package/scope-tailwind
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-" ')
|
||||
|
||||
// tsup to build src2/ into out/
|
||||
execSync('npx tsup')
|
||||
|
||||
|
||||
console.log('✅ Done building! Press Cmd+Shift+B again.')
|
||||
console.log('✅ Done building! Kill your build script(s) (Ctrl+D in them), then press Cmd+Shift+B again.')
|
||||
|
||||
@@ -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,3 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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";
|
||||
|
||||
+15
-12
@@ -1,3 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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'
|
||||
@@ -16,6 +21,9 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const inlineDiffService = useService('inlineDiffService')
|
||||
|
||||
const clipboardService = useService('clipboardService')
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
@@ -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
|
||||
@@ -46,7 +49,7 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={async () => {
|
||||
|
||||
inlineDiffService.startStreaming('ctrl+l', text)
|
||||
inlineDiffService.startStreaming({ featureName: 'Ctrl+L' }, text)
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@@ -127,7 +130,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>
|
||||
@@ -211,7 +214,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'}`}>
|
||||
<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,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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 { useSettingsState, useService, useSidebarState, useThreadsState } from '../util/services.js';
|
||||
import { generateCtrlLInstructions, generateDiffInstructions } from '../../../prompt/systemPrompts.js';
|
||||
import { userInstructionsStr } from '../../../prompt/stringifySelections.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { MarkdownRender } from '../markdown/MarkdownRender.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.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 { VoidInputBox } from '../util/inputs.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
|
||||
|
||||
const IconX = ({ size, className = '' }: { size: number, className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='black'
|
||||
className={className}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
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 +141,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 +153,85 @@ 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-4 p-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={`grid grid-rows-2 gap-1 relative
|
||||
select-none
|
||||
bg-vscode-badge-bg border border-vscode-button-border rounded-md
|
||||
w-fit h-fit min-w-[81px] p-1
|
||||
`}
|
||||
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='truncate'>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
|
||||
{/* type of selection */}
|
||||
<span className='truncate text-opacity-75'>{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'>
|
||||
<BlockCode text={selection.selectionStr!} />
|
||||
</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 +239,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={`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,7 +257,7 @@ 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')
|
||||
|
||||
@@ -154,36 +267,35 @@ export const SidebarChat = () => {
|
||||
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')
|
||||
|
||||
// ----- 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 llmMessageService = useService('llmMessageService')
|
||||
|
||||
// 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 isDisabled = !instructions
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
@@ -191,165 +303,239 @@ 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: generateCtrlLInstructions }
|
||||
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: userInstructionsStr(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] ${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={
|
||||
// // 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 input and textarea elements
|
||||
// .join(' ') +
|
||||
// ` outline-none`
|
||||
// .split(' ')
|
||||
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`) // apply styles to ancestor input and textarea elements
|
||||
// .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- @@[&_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='w-[250px]'>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isLoading ?
|
||||
// stop button
|
||||
<button
|
||||
className={`size-[20px] rounded-full bg-white cursor-pointer flex items-center justify-center`}
|
||||
onClick={onAbort}
|
||||
type='button'
|
||||
>
|
||||
<IconSquare size={16} className="stroke-[2]" />
|
||||
</button>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<button
|
||||
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
|
||||
${isDisabled ?
|
||||
'bg-vscode-disabled-fg' // cursor-not-allowed
|
||||
: 'bg-white' // cursor-pointer
|
||||
}
|
||||
`}
|
||||
disabled={isDisabled}
|
||||
type='submit'
|
||||
>
|
||||
<IconArrowUp size={20} className="stroke-[2]" />
|
||||
</button>
|
||||
}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
+17
-11
@@ -1,7 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
@@ -23,10 +24,10 @@ export const SidebarThreadSelector = () => {
|
||||
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 +49,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 +57,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,3 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -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,278 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { useService } from '../util/services.js';
|
||||
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { 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 { DomScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js';
|
||||
import { ScrollableElementCreationOptions } from '../../../../../../../base/browser/ui/scrollbar/scrollableElementOptions.js';
|
||||
|
||||
|
||||
|
||||
export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, propsFn, dispose, onCreateInstance, children, className }
|
||||
: {
|
||||
ctor: { new(...params: CtorParams): Instance },
|
||||
propsFn: (container: HTMLDivElement) => CtorParams,
|
||||
onCreateInstance: (instance: Instance) => IDisposable[],
|
||||
dispose: (instance: Instance) => void,
|
||||
children?: React.ReactNode,
|
||||
className?: string
|
||||
}
|
||||
) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const instance = new ctor(...propsFn(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 contextViewProvider = useService('contextViewService');
|
||||
|
||||
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 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 contextViewProvider = useService('contextViewService');
|
||||
|
||||
let containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return <WidgetComponent
|
||||
className='text-ellipsis whitespace-nowrap pr-6'
|
||||
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])}
|
||||
|
||||
/>;
|
||||
};
|
||||
|
||||
|
||||
// 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;
|
||||
// }) => {
|
||||
// const contextViewProvider = useService('contextViewService');
|
||||
// const contextMenuProvider = useService('contextMenuService');
|
||||
|
||||
|
||||
// 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);
|
||||
|
||||
// const themeService = useService('themeService');
|
||||
// const contextViewService = useService('contextViewService');
|
||||
// const hoverService = useService('hoverService');
|
||||
|
||||
// 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,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js';
|
||||
|
||||
|
||||
export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLElement, services: ReactServicesType) => {
|
||||
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, services: ReactServicesType) => {
|
||||
if (typeof document === 'undefined') {
|
||||
console.error('index.tsx error: document was undefined')
|
||||
return
|
||||
}
|
||||
|
||||
_registerServices(services)
|
||||
const disposables = _registerServices(services)
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(<Component />);
|
||||
root.render(<Component />); // 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,63 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ConfigState } from '../../../registerConfig.js'
|
||||
import { VoidSidebarState, ReactServicesType } from '../../../registerSidebar.js'
|
||||
import { ThreadsState } from '../../../registerThreads.js'
|
||||
import { ThreadsState } from '../../../threadHistoryService.js'
|
||||
import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { RefreshModelState } from '../../../../../../../platform/void/common/refreshModelService.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.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, these variables are always updated to the latest state:
|
||||
// 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 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: RefreshModelState
|
||||
const refreshModelStateListeners: Set<(s: RefreshModelState) => 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 = (services_: ReactServicesType) => {
|
||||
|
||||
if (wasCalled) console.error(`void _registerServices was called again! It should only be called once.`)
|
||||
const disposables: IDisposable[] = []
|
||||
|
||||
// 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
|
||||
const { sidebarStateService, settingsStateService, threadsStateService, refreshModelService, themeService } = services
|
||||
|
||||
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(() => {
|
||||
refreshModelState = refreshModelService.state
|
||||
refreshModelStateListeners.forEach(l => l(refreshModelState))
|
||||
})
|
||||
)
|
||||
|
||||
colorThemeState = themeService.getColorTheme().type
|
||||
disposables.push(
|
||||
themeService.onDidColorThemeChange(theme => {
|
||||
colorThemeState = theme.type
|
||||
colorThemeStateListeners.forEach(l => l(colorThemeState))
|
||||
})
|
||||
)
|
||||
|
||||
return disposables
|
||||
}
|
||||
|
||||
|
||||
// -- services --
|
||||
export const useService = <T extends keyof ReactServicesType,>(serviceName: T) => {
|
||||
export const useService = <T extends keyof ReactServicesType,>(serviceName: T): ReactServicesType[T] => {
|
||||
if (services === null) {
|
||||
throw new Error('useAccessor must be used within an AccessorProvider')
|
||||
}
|
||||
return services[serviceName] as ReactServicesType[T]
|
||||
return services[serviceName]
|
||||
}
|
||||
|
||||
// -- state of services --
|
||||
@@ -72,12 +116,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 +135,32 @@ 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 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,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, useService } from '../util/services.js'
|
||||
import { VoidSelectBox } from '../util/inputs.js'
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
|
||||
|
||||
const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const voidSettingsService = useService('settingsStateService')
|
||||
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 = () => {
|
||||
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,274 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidInputBox, VoidSelectBox } from '../util/inputs.js'
|
||||
import { useIsDark, useRefreshModelState, useService, useSettingsState } from '../util/services.js'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
|
||||
|
||||
// models
|
||||
|
||||
const RefreshableModels = () => {
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const refreshModelState = useRefreshModelState()
|
||||
const refreshModelService = useService('refreshModelService')
|
||||
|
||||
if (!settingsState.settingsOfProvider.ollama.enabled)
|
||||
return null
|
||||
|
||||
return <div>
|
||||
<button onClick={() => refreshModelService.refreshOllamaModels()}>refresh Ollama built-in models</button>
|
||||
{refreshModelState === 'loading' ? 'loading...' : 'good!'}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
||||
const settingsStateService = useService('settingsStateService')
|
||||
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: titleOfProviderName(providerName), value: providerName })), [providerNames])
|
||||
|
||||
return <>
|
||||
<div className='flex justify-center 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 w-full'>
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
|
||||
{!errorString ? null : <div className='text-center text-red-500'>
|
||||
{errorString}
|
||||
</div>}
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
const AddModelButton = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return <>
|
||||
{open ?
|
||||
<AddModelMenu onSubmit={() => { setOpen(false) }} />
|
||||
: <button onClick={() => setOpen(true)}>Add Model</button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
export const ModelDump = () => {
|
||||
|
||||
const settingsStateService = useService('settingsStateService')
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
// a dump of all the enabled providers' models
|
||||
const modelDump: (ModelInfo & { 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 })))
|
||||
}
|
||||
|
||||
return <div className=''>
|
||||
{modelDump.map(m => {
|
||||
const { isHidden, isDefault, modelName, providerName, providerEnabled } = m
|
||||
|
||||
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>
|
||||
<button disabled={!providerEnabled} onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{(!providerEnabled || isHidden) ? '❌' : '✅'}</button>
|
||||
<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, placeholder } = displayInfoOfSettingName(providerName, settingName)
|
||||
const voidSettingsService = useService('settingsStateService')
|
||||
|
||||
|
||||
let weChangedTextRef = false
|
||||
|
||||
return <><ErrorBoundary>
|
||||
<label>{title}</label>
|
||||
<VoidInputBox
|
||||
placeholder={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
|
||||
}
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
multiline={false}
|
||||
/>
|
||||
</ErrorBoundary></>
|
||||
|
||||
}
|
||||
|
||||
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
const voidSettingsService = useService('settingsStateService')
|
||||
|
||||
const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName]
|
||||
|
||||
return <>
|
||||
|
||||
<div className='flex items-center gap-4'>
|
||||
<h3 className='text-xl'>{titleOfProviderName(providerName)}</h3>
|
||||
<button onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}>{enabled ? '✅' : '❌'}</button>
|
||||
</div>
|
||||
{/* settings besides models (e.g. api key) */}
|
||||
{Object.keys(others).map((sName, i) => {
|
||||
const settingName = sName as keyof typeof others
|
||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
export const VoidProviderSettings = () => {
|
||||
return <>
|
||||
{providerNames.map(providerName =>
|
||||
<SettingsForProvider key={providerName} providerName={providerName} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
|
||||
onClick={() => { setTab('models') }}
|
||||
>Models</button>
|
||||
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
|
||||
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`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<ModelDump />
|
||||
<AddModelButton />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
|
||||
</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,122 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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-activeBorder)",
|
||||
"input-option-active-fg": "var(--vscode-activeForeground)",
|
||||
"input-option-hover-bg": "var(--vscode-hoverBackground)",
|
||||
"input-validation-error-bg": "var(--vscode-errorBackground)",
|
||||
"input-validation-error-fg": "var(--vscode-errorForeground)",
|
||||
"input-validation-error-border": "var(--vscode-errorBorder)",
|
||||
"input-validation-info-bg": "var(--vscode-infoBackground)",
|
||||
"input-validation-info-fg": "var(--vscode-infoForeground)",
|
||||
"input-validation-info-border": "var(--vscode-infoBorder)",
|
||||
"input-validation-warning-bg": "var(--vscode-warningBackground)",
|
||||
"input-validation-warning-fg": "var(--vscode-warningForeground)",
|
||||
"input-validation-warning-border": "var(--vscode-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)",
|
||||
|
||||
// editorwidget colors
|
||||
"editorwidget-fg": "var(--vscode-editorWidget-foreground)",
|
||||
"editorwidget-bg": "var(--vscode-editorWidget-background)",
|
||||
"editorwidget-border": "var(--vscode-editorWidget-border)",
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/diff/index.tsx',
|
||||
],
|
||||
outDir: './out',
|
||||
format: ['esm'],
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
+54
-26
@@ -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,18 @@ 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 { CodeStagingSelection, IThreadHistoryService } from './threadHistoryService.js';
|
||||
// import { IEditorService } from '../../../services/editor/common/editorService.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 { OPEN_VOID_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
@@ -61,27 +63,45 @@ registerAction2(class extends Action2 {
|
||||
if (!model)
|
||||
return
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
metricsService.capture('Chat Navigation', { 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 selectionRange = roundRangeToLines(
|
||||
accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
)
|
||||
|
||||
|
||||
if (selectionRange) {
|
||||
const selection: CodeStagingSelection = {
|
||||
selectionStr: getContentInRange(model, selectionRange),
|
||||
fileURI: model.uri
|
||||
|
||||
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 +109,7 @@ registerAction2(class extends Action2 {
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add
|
||||
else {
|
||||
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
|
||||
}
|
||||
@@ -103,16 +124,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 +147,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(OPEN_VOID_SETTINGS_ACTION_ID)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { getReactServices } from './helpers/reactServicesHelper.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
|
||||
// import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
|
||||
// import { Codicon } from '../../../../base/common/codicons.js';
|
||||
// import { Codicon } from '../../../../base/common/codicons.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,
|
||||
) {
|
||||
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 => {
|
||||
const services = getReactServices(accessor)
|
||||
|
||||
// mount react
|
||||
const disposables: IDisposable[] | undefined = mountSidebar(parent, services);
|
||||
disposables?.forEach(d => this._register(d))
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
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 { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_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;
|
||||
|
||||
openSidebarView(): 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(
|
||||
@IViewsService private readonly _viewsService: IViewsService,
|
||||
) {
|
||||
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.openSidebarView()
|
||||
}
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
openSidebarView() {
|
||||
this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
this._viewsService.openView(VOID_VIEW_ID);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 chat
|
||||
import './registerSidebar.js'
|
||||
// register Sidebar pane, state, actions (keybinds, menus)
|
||||
import './sidebarActions.js'
|
||||
import './sidebarPane.js'
|
||||
import './sidebarStateService.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,162 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { getReactServices } from './helpers/reactServicesHelper.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';
|
||||
|
||||
readonly resource = URI.from({
|
||||
scheme: 'void-editor-settings',
|
||||
path: 'void-settings' // Give it a unique path
|
||||
});
|
||||
|
||||
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 services = getReactServices(accessor);
|
||||
const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, services);
|
||||
|
||||
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 OPEN_VOID_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
|
||||
// register the gear on the top right
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: OPEN_VOID_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 input = instantiationService.createInstance(VoidSettingsInput);
|
||||
await editorService.openEditor(input);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// add to settings gear on bottom left
|
||||
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
|
||||
group: '0_command',
|
||||
command: {
|
||||
id: OPEN_VOID_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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user