Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3de9faed5c | |||
| c179912368 | |||
| 89c2adee70 | |||
| c4b9637546 | |||
| 868736330a | |||
| 2c856c9932 | |||
| a3b49d8aa2 | |||
| da9817bd99 | |||
| 58b4d90b49 | |||
| 39ea63b207 | |||
| b02b2f0c89 | |||
| 902d419026 | |||
| b244f2f08b | |||
| 4d9b52ede6 | |||
| e94d2de641 | |||
| 5fdc5d6df2 | |||
| cf5ff98081 | |||
| 45eeb14b54 | |||
| dcfb5d443c | |||
| f839e73ebc | |||
| c775d34d75 | |||
| 3023ed87d2 | |||
| 3a5a04ceae | |||
| 22475f7206 | |||
| 68333ce349 | |||
| 9f3dbc3eb4 | |||
| 10949b44d1 | |||
| 45778d5735 | |||
| 816d6090cc | |||
| 355d8d9a5a | |||
| b47e8f2237 | |||
| 0543b2c893 | |||
| 69cf58af5a | |||
| f9b3dbe316 | |||
| 176b988539 | |||
| 11cc9f8b4e | |||
| ab396d4c1d | |||
| a01ca97b18 | |||
| 1b4105ed2b | |||
| 4024ed40c2 | |||
| 8c53878681 | |||
| 7bd52438f5 | |||
| 6f2d513d3f | |||
| 800aeea247 | |||
| 021acf541a | |||
| 7a739e567c | |||
| ef52ebfdc2 | |||
| e83a172816 | |||
| c3ef7e616f | |||
| f77907fe0d | |||
| 6d28b0627f | |||
| 7d3ce48f5f | |||
| 97c611f4f2 | |||
| 90d94ecd3c | |||
| 9f7154ddab | |||
| 5cdb4b0c47 | |||
| a61fdd0419 | |||
| b7db527866 | |||
| c921059690 | |||
| cee6dcc524 | |||
| b9e097bfc0 | |||
| 87c6669020 | |||
| 80448c4471 | |||
| b4f9c1fa97 | |||
| 10052819ce | |||
| 0f2ebecbd3 | |||
| eb68e1d294 | |||
| 17aa9eea97 | |||
| 5e85d804f8 | |||
| 8f2a2d642f | |||
| 6141351e15 | |||
| 00e196a55f | |||
| 16913e96e5 | |||
| 6f55e6900b | |||
| 36e10dcc08 | |||
| ce8c790e5c | |||
| 738067951a | |||
| 8b1a42499b | |||
| 7fd52b4805 | |||
| 5f30506f46 | |||
| 48efc32dd3 | |||
| 5fffd2fe0f | |||
| 664428c8f0 | |||
| aa8b999da0 | |||
| d99d2cca4a | |||
| e7da1d27bd | |||
| 56c50dc218 | |||
| e0e7b360fe | |||
| 434a8289d9 | |||
| 7e0e65d3b7 | |||
| 4cb96cb60e | |||
| 4a7ea3ec43 | |||
| bd53772523 | |||
| f09f538b05 | |||
| ff2ed836c7 | |||
| 6db29526ec | |||
| 27d9cc2916 | |||
| dbbaa61961 | |||
| 05bd1790a7 | |||
| 4b30990e92 | |||
| 3a0eedc9fa | |||
| 64d4e264bf | |||
| afc5cf512b | |||
| 35e8cf59ff | |||
| fd4ecaff8a | |||
| 55c4f9e1cf | |||
| 78f9463da1 | |||
| 23f1e8c641 | |||
| 79eacad18b | |||
| 3e0e0bd4aa | |||
| fece3b2548 | |||
| 091855a89a | |||
| b3bd454bad | |||
| bd580946ce | |||
| 3bd6d08841 | |||
| 152cfd93aa | |||
| 7ea4e16f5f | |||
| 884548615b | |||
| b62943fffd | |||
| 9bd97d0cd5 | |||
| bf9852858e | |||
| 6058b7ff78 | |||
| 9b8f3ec2e2 | |||
| d3fd5be294 | |||
| aa4398c734 | |||
| 29d1a919be | |||
| feea5e13f3 | |||
| 119db73f69 | |||
| f59b24496e | |||
| a2ae9794ed | |||
| f335ce7204 | |||
| 0ddab03f8b | |||
| 5f8fddb01e | |||
| c81f628b7b | |||
| 9d1ab8728a | |||
| 7a0f9ac77c | |||
| a0fa104d23 | |||
| 7fd0b133e5 | |||
| 321876932a | |||
| eda8517c78 | |||
| 6ce37f7627 | |||
| 18b76885f5 | |||
| ba8644fbb6 | |||
| ee959ded08 | |||
| 499a47904e | |||
| 5ac03bb917 | |||
| 922f3ba2d7 | |||
| a189629341 | |||
| d2098f71eb | |||
| 77c59f2e80 | |||
| e6ecad3f65 | |||
| ee43ec99ea | |||
| e77039e9ad | |||
| 6aa1375067 | |||
| fe017948c6 | |||
| 9b3d4cbdba | |||
| a8cf0b361f | |||
| 680125baec | |||
| 0db4f7118d | |||
| 2ea20243a8 | |||
| 4d02381dab | |||
| a80883c10f | |||
| 20b0fd6af9 | |||
| 5171f7244f | |||
| 43708d25e7 | |||
| 2b54733242 | |||
| cd77d1a1c4 | |||
| 37af09add8 | |||
| ade1e70536 | |||
| 159e0b677f |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: Issue
|
||||
about: Submit an Issue to Void
|
||||
title: For VSCode-related issues (eg builds), please start the title with `[App]`. Otherwise, start it with `[Bug]` or `[Feature]`.
|
||||
---
|
||||
|
||||
1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable.
|
||||
|
||||
2. Describe the issue/feature here.
|
||||
@@ -119,7 +119,7 @@ workspace/
|
||||
```
|
||||
|
||||
### Distributing
|
||||
Void's maintainers distribute Void on our website and in releases. If you'd like to see the scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution, feel free to reach out.
|
||||
Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor/void-builder).
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
|
||||
@@ -27,14 +27,14 @@ This repo contains the full sourcecode for Void. We are currently in [open beta]
|
||||
|
||||
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
|
||||
|
||||
2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
2. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
3. We're open to collaborations and suggestions of all types - just reach out.
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [our Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
|
||||
|
||||
## Support
|
||||
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
|
||||
|
||||
+34
-25
@@ -4,47 +4,53 @@ The Void codebase is not as intimidating as it seems!
|
||||
|
||||
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
The purpose of this document is to explain how Void's codebase works. If you want build instructions, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
The purpose of this document is to explain how Void's codebase works. If you want build instructions instead, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Void Codebase Guide
|
||||
|
||||
### Terminology
|
||||
### VSCode Rundown
|
||||
Here's a VSCode rundown if you're just getting started with Void. You can also see Microsoft's [wiki](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for some pictures. VSCode is an Electron app. Electron runs two processes: a **main** process (for internals) and a **browser** process (browser means HTML in general, not just "web browser").
|
||||
<p align="center" >
|
||||
<img src="https://github.com/user-attachments/assets/eef80306-2bfe-4cac-ba15-6156f65ab3bb" alt="Credit - https://github.com/microsoft/vscode/wiki/Source-Code-Organization" width="700px">
|
||||
</p>
|
||||
|
||||
Here is some important terminology you should know if you're working inside VSCode:
|
||||
- An **Editor** is the thing that you type your code in. If you have 10 tabs open, that's just one editor! Editors contain tabs (or "models").
|
||||
- A **Model** is an internal representation of a file's contents. It's shared between editors (for example, if you press `Cmd+\` to make a new editor, then the model of a file like `A.ts` is shared between them. Two editors, one model. That's how changes sync.).
|
||||
- Each model has a **URI** it represents, like `/Users/.../my_file.txt`. (A URI or "resource" is generally just a path).
|
||||
- The **Workbench** is the wrapper that contains all the editors, the terminal, the file system tree, etc.
|
||||
- Usually you use the `ITextModel` type for models and the `ICodeEditor` type for editors. There aren't that many other types.
|
||||
|
||||
|
||||
|
||||
### Minimal VSCode Rundown
|
||||
Here's a minimal VSCode rundown if you're just getting started with Void:
|
||||
|
||||
- VSCode is (and therefore Void is) an Electron app. Electron runs two processes: a **main** process (for internals) and a **browser** process (browser means HTML in general, not just "web browser").
|
||||
- Code in a `browser/` folder always lives on the browser process, and it can use `window` and other browser items.
|
||||
- Code in an `electron-main/` folder always lives on the main process, and it can import `node_modules`.
|
||||
- Code in `common/` can be used by either process, but doesn't get any special imports.
|
||||
- The browser environment is not allowed to import `node_modules`, but there are two workarounds:
|
||||
- The browser environment is not allowed to import `node_modules`. We came up with two workarounds:
|
||||
1. Bundle the raw node_module code to the browser - we're doing this for React.
|
||||
2. Implement the code on `electron-main/` and set up a channel between main/browser - we're doing this for sendLLMMessage.
|
||||
|
||||
|
||||
|
||||
VSCode is organized into "Services". A service is just a class that mounts a single time (in computer science theory this is called a "singleton"). You can register services with `registerSingleton` so that you can easily use them in any constructor with `@<Service>`. See _dummyContrib for an example we put together on how to register them. The registration is the same every time.
|
||||
|
||||
Services are always lazily created, even if you register them as Eager. If you want something that always runs on Void's mount, you should use a "workbench contribution". See _dummyContrib for this. Very similar to a Service, just registered slightly differently.
|
||||
### Terminology
|
||||
|
||||
Actions or "commands" are functions you register on VSCode so that either you or the user can call them later. You can run actions as a user by pressing Cmd+Shift+P (opens the command pallete), or you can run them internally by using the commandService to call them by ID. We use actions to register keybinding listeners like Cmd+L, Cmd+K, etc. The nice thing about actions is the user can change the keybindings.
|
||||
Here's some terminology you might want to know about when working inside VSCode:
|
||||
- An **Editor** is the thing that you type your code in. If you have 10 tabs open, that's just one editor! Editors contain tabs (or "models").
|
||||
- A **Model** is an internal representation of a file's contents. It's shared between editors (for example, if you press `Cmd+\` to make a new editor, then the model of a file like `A.ts` is shared between them. Two editors, one model. That's how changes sync.).
|
||||
- Each model has a **URI** it represents, like `/Users/.../my_file.txt`. (A URI or "resource" is generally just a path).
|
||||
- The **Workbench** is the wrapper that contains all the editors, the terminal, the file system tree, etc.
|
||||
- Usually you use the `ITextModel` type for models and the `ICodeEditor` type for editors. There aren't that many other types.
|
||||
<p align="center" >
|
||||
<img src="https://github.com/user-attachments/assets/6521c228-dc96-4cf5-a673-6b9ca78b9b06" alt="Credit - https://code.visualstudio.com/docs/getstarted/userinterface" width="400px">
|
||||
</p>
|
||||
|
||||
|
||||
See [here](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for a decent VSCode guide with even more info.
|
||||
|
||||
- VSCode is organized into "**Services**". A service is just a class that mounts a single time (in computer science theory this is called a "singleton"). You can register services with `registerSingleton` so that you can easily use them in any constructor with `@<Service>`. See _dummyContrib for an example we put together on how to register them. The registration is the same every time.
|
||||
|
||||
- "**Actions**" are functions you register on VSCode so that either you or the user can call them later. They're also called "**Commands**".
|
||||
- You can run actions as a user by pressing Cmd+Shift+P (opens the command pallete), or you can run them internally by using the commandService to call them by ID. We use actions to register keybinding listeners like Cmd+L, Cmd+K, etc. The nice thing about actions is the user can change the keybindings.
|
||||
|
||||
Each section below contains an overview of a core part of Void's sourcecode. You might want to scroll to find the item that's relevant to you.
|
||||
|
||||
### Internal LLM Message Pipeline
|
||||
|
||||
@@ -80,13 +86,16 @@ This is what allows Void to quickly apply code even on 1000-line files. It's the
|
||||
The `editCodeService` file runs Apply. The same exact code is also used when the LLM calls the Edit tool, and when you submit Cmd+K. Just different versions of Fast/Slow Apply mode.
|
||||
|
||||
Here is some important terminology:
|
||||
- A **DiffZone** is a {startLine, endLine} region in which we show Diffs (red/green areas). We update it when the user types, so it's always accurate.
|
||||
- A **DiffArea** is a generalization that tracks line numbers like a DiffZone.
|
||||
- The only type of zone that can "stream" is a DiffZone. Each DiffZone has an llmCancelToken if it's streaming.
|
||||
- A **DiffZone** is a {startLine, endLine} region of text where we compute and show red/green areas, or **Diffs**. When any changes are made to a file, we loop through all the DiffAreas on that file and refresh its Diffs.
|
||||
- A **DiffArea** is a generalization that just tracks line numbers like a DiffZone.
|
||||
- The only type of DiffArea that can "stream" is a DiffZone. Each DiffZone has an llmCancelToken if it's streaming.
|
||||
|
||||
How Apply works:
|
||||
- When you click Apply, we create a **DiffZone** over that the full file so that any changes that the LLM makes will show up in red/green. We then stream the change.
|
||||
- When an LLM calls Edit, it's really calling Apply.
|
||||
- When you submit Cmd+K, it's the same as Apply except we create a smaller DiffZone (not on the whole file).
|
||||
|
||||
|
||||
### Writing Files Inner Workings
|
||||
When Void wants to change your code, it just writes to a text model. This means all you need to know to write to a file is its URI - you don't have to load it, save it, etc. There are some annoying background URI/model things to think about to get this to work, but we handled them all in `voidModelService`.
|
||||
|
||||
@@ -121,7 +130,7 @@ If you want to know how our build pipeline works, see our build repo [here](http
|
||||
|
||||
|
||||
|
||||
## VSCode Codebase Guide (Not Void)
|
||||
## VSCode Codebase Guide
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode, the foundation of Void. We hope it's helpful!
|
||||
|
||||
|
||||
Generated
+6388
-3938
File diff suppressed because it is too large
Load Diff
@@ -77,6 +77,7 @@
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
@@ -102,6 +103,7 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.98",
|
||||
"@xterm/headless": "^5.6.0-beta.98",
|
||||
"@xterm/xterm": "^5.6.0-beta.98",
|
||||
"ajv": "^8.17.1",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
@@ -109,6 +111,7 @@
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.4",
|
||||
"katex": "^0.16.22",
|
||||
"kerberos": "2.1.1",
|
||||
"lucide-react": "^0.477.0",
|
||||
"marked": "^15.0.7",
|
||||
@@ -144,6 +147,7 @@
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/gulp-svgmin": "^1.2.1",
|
||||
"@types/http-proxy-agent": "^2.0.1",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/kerberos": "^1.1.2",
|
||||
"@types/minimist": "^1.2.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.0.3",
|
||||
"voidVersion": "1.2.6",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
|
||||
@@ -34,11 +34,8 @@ mkdir -pv "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
|
||||
# Exclude create-appimage.sh and appimagetool-x86_64.AppImage from being copied
|
||||
echo "Copying files excluding create-appimage.sh and appimagetool-x86_64.AppImage..."
|
||||
for file in ./*; do
|
||||
if [[ "$file" != "./create-appimage.sh" && "$file" != "./appimagetool-x86_64.AppImage" ]]; then
|
||||
cp -rv "$file" "$APP_DIR/usr/bin/"
|
||||
fi
|
||||
done
|
||||
|
||||
cp -v ./void "$APP_DIR/usr/bin/"
|
||||
|
||||
# Copy the icon to required locations
|
||||
cp -v ./void.png "$APP_DIR/void.png"
|
||||
@@ -1,3 +1,16 @@
|
||||
|
||||
|
||||
# README
|
||||
|
||||
This is a community-made AppImage creation script.
|
||||
|
||||
There are some reported bugs with it.
|
||||
|
||||
To generate an AppImage yourself, feel free to look at
|
||||
stable-linux.yml in the separate `void-builder/` repo,
|
||||
which runs a GitHub Action that builds the AppImage you see on our website.
|
||||
|
||||
|
||||
# Void AppImage Creation Script
|
||||
|
||||
This script automates the process of creating an AppImage for the Void Editor using Docker. It works on macOS and Linux platforms.
|
||||
|
||||
@@ -22,10 +22,12 @@ import { ColorScheme } from '../../web.api.js';
|
||||
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
|
||||
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
|
||||
import { splitRecentLabel } from '../../../../base/common/labels.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
|
||||
/* eslint-disable */ // Void
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
|
||||
import { VIEWLET_ID as REMOTE_EXPLORER_VIEWLET_ID } from '../../../contrib/remote/browser/remoteExplorer.js';
|
||||
/* eslint-enable */
|
||||
|
||||
// interface WatermarkEntry {
|
||||
@@ -98,6 +100,7 @@ export class EditorGroupWatermark extends Disposable {
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -182,14 +185,21 @@ export class EditorGroupWatermark extends Disposable {
|
||||
// Void - if the workbench is empty, show open
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
|
||||
// Create a flex container for buttons with vertical direction
|
||||
const buttonContainer = $('div');
|
||||
buttonContainer.style.display = 'flex';
|
||||
buttonContainer.style.flexDirection = 'column'; // Change to column for vertical stacking
|
||||
buttonContainer.style.alignItems = 'center'; // Center the buttons horizontally
|
||||
buttonContainer.style.gap = '8px'; // Reduce gap between buttons from 16px to 8px
|
||||
buttonContainer.style.marginBottom = '16px';
|
||||
voidIconBox.appendChild(buttonContainer);
|
||||
|
||||
// Open a folder
|
||||
const openFolderButton = h('button')
|
||||
openFolderButton.root.classList.add('void-watermark-button')
|
||||
openFolderButton.root.classList.add('void-openfolder-button')
|
||||
openFolderButton.root.style.display = 'block'
|
||||
openFolderButton.root.style.marginLeft = 'auto'
|
||||
openFolderButton.root.style.marginRight = 'auto'
|
||||
openFolderButton.root.style.marginBottom = '16px'
|
||||
openFolderButton.root.textContent = 'Open a folder'
|
||||
openFolderButton.root.style.width = '124px' // Set width to 124px as requested
|
||||
openFolderButton.root.textContent = 'Open Folder'
|
||||
openFolderButton.root.onclick = () => {
|
||||
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
|
||||
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
|
||||
@@ -198,7 +208,19 @@ export class EditorGroupWatermark extends Disposable {
|
||||
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
|
||||
// }
|
||||
}
|
||||
voidIconBox.appendChild(openFolderButton.root);
|
||||
buttonContainer.appendChild(openFolderButton.root);
|
||||
|
||||
// Open SSH button
|
||||
const openSSHButton = h('button')
|
||||
openSSHButton.root.classList.add('void-openssh-button')
|
||||
openSSHButton.root.style.display = 'block'
|
||||
openSSHButton.root.style.backgroundColor = '#5a5a5a' // Made darker than the default gray
|
||||
openSSHButton.root.style.width = '124px' // Set width to 124px as requested
|
||||
openSSHButton.root.textContent = 'Open SSH'
|
||||
openSSHButton.root.onclick = () => {
|
||||
this.viewsService.openViewContainer(REMOTE_EXPLORER_VIEWLET_ID);
|
||||
}
|
||||
buttonContainer.appendChild(openSSHButton.root);
|
||||
|
||||
|
||||
// Recents
|
||||
@@ -244,6 +266,9 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
const dirSpan = $('span');
|
||||
dirSpan.style.paddingLeft = '4px';
|
||||
dirSpan.style.whiteSpace = 'nowrap';
|
||||
dirSpan.style.overflow = 'hidden';
|
||||
dirSpan.style.maxWidth = '300px';
|
||||
dirSpan.innerText = parentPath;
|
||||
dirSpan.title = fullPath;
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
}
|
||||
|
||||
/* light */
|
||||
.void-void-icon,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
@@ -60,14 +61,17 @@
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
}
|
||||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
}
|
||||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ registerAction2(class extends Action2 {
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
console.log('hi')
|
||||
const n = accessor.get(IDummyService)
|
||||
console.log('Hi', n._serviceBrand)
|
||||
}
|
||||
@@ -59,6 +58,6 @@ class DummyService extends Disposable implements IWorkbenchContribution, IDummyS
|
||||
|
||||
|
||||
// pick one and delete the other:
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager); // lazily loaded, even if Eager
|
||||
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); // mounts on start
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
// // const result = await new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search_files'],
|
||||
// // tools: ['search_for_files'],
|
||||
// // onFinalMessage: ({ result: r, }) => {
|
||||
// // res(r)
|
||||
// // },
|
||||
@@ -73,7 +73,7 @@
|
||||
// // const result = new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search_files'],
|
||||
// // tools: ['search_for_files'],
|
||||
// // onResult: (r) => {
|
||||
// // res(r)
|
||||
// // }
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { FeatureName } from '../common/voidSettingsTypes.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
// import { IContextGatheringService } from './contextGatheringService.js';
|
||||
|
||||
|
||||
@@ -792,17 +793,18 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
const requestId = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'FIMMessage',
|
||||
messages: {
|
||||
prefix: llmPrefix,
|
||||
suffix: llmSuffix,
|
||||
stopTokens: stopTokens,
|
||||
},
|
||||
messages: this._convertToLLMMessageService.prepareFIMMessage({
|
||||
messages: {
|
||||
prefix: llmPrefix,
|
||||
suffix: llmSuffix,
|
||||
stopTokens: stopTokens,
|
||||
}
|
||||
}),
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
@@ -890,6 +892,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService
|
||||
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -11,35 +11,63 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, ToolName, toolCallXMLStr, } from '../common/prompt/prompts.js';
|
||||
import { getErrorMessage, LLMChatMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
|
||||
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { IToolsService } from './toolsService.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolMessage } from '../common/chatThreadServiceTypes.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { shorten } from '../../../../base/common/labels.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { findLast, findLastIdx } from '../../../../base/common/arraysFind.js';
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js';
|
||||
import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { IDirectoryStrService } from './directoryStrService.js';
|
||||
import { truncate } from '../../../../base/common/strings.js';
|
||||
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
import { timeout } from '../../../../base/common/async.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
|
||||
|
||||
// related to retrying when LLM message has error
|
||||
const CHAT_RETRIES = 3
|
||||
const RETRY_DELAY = 2500
|
||||
|
||||
|
||||
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
const s = currentSelections[i]
|
||||
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
|
||||
if (s.type === 'File' && newSelection.type === 'File') {
|
||||
return i
|
||||
}
|
||||
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
// if there's any collision return true
|
||||
const [oldStart, oldEnd] = s.range
|
||||
const [newStart, newEnd] = newSelection.range
|
||||
if (oldStart !== newStart || oldEnd !== newEnd) continue
|
||||
return i
|
||||
}
|
||||
if (s.type === 'Folder' && newSelection.type === 'Folder') {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Store a checkpoint of all "before" files on each x.
|
||||
@@ -71,7 +99,7 @@ const defaultMessageState: UserMessageState = {
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
|
||||
type ThreadType = {
|
||||
export type ThreadType = {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
@@ -159,6 +187,10 @@ export interface IChatThreadService {
|
||||
openNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
// thread selector
|
||||
deleteThread(threadId: string): void;
|
||||
duplicateThread(threadId: string): void;
|
||||
|
||||
// exposed getters/setters
|
||||
// these all apply to current thread
|
||||
getCurrentMessageState: (messageIdx: number) => UserMessageState
|
||||
@@ -170,6 +202,10 @@ export interface IChatThreadService {
|
||||
getCurrentFocusedMessageIdx(): number | undefined;
|
||||
isCurrentlyFocusingMessage(): boolean;
|
||||
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
dangerousSetState: (newState: ThreadsState) => void;
|
||||
resetState: () => void;
|
||||
|
||||
// // current thread's staging selections
|
||||
// closeCurrentStagingSelectionsInMessage(opts: { messageIdx: number }): void;
|
||||
// closeCurrentStagingSelectionsInThread(): void;
|
||||
@@ -221,17 +257,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
@IToolsService private readonly _toolsService: IToolsService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
|
||||
@ITerminalToolService private readonly _terminalToolService: ITerminalToolService,
|
||||
@IMetricsService private readonly _metricsService: IMetricsService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
|
||||
@IEditCodeService private readonly _editCodeService: IEditCodeService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IDirectoryStrService private readonly _directoryStrService: IDirectoryStrService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
@@ -247,9 +278,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
// always be in a thread
|
||||
this.openNewThread()
|
||||
|
||||
// when the user changes files, automatically add the new file as a stagingSelection
|
||||
this._register(this._editorService.onDidActiveEditorChange(() => this._addCurrentFileAsStagingSelectionDuringFileChange()));
|
||||
|
||||
|
||||
// keep track of user-modified files
|
||||
// const disposablesOfModelId: { [modelId: string]: IDisposable[] } = {}
|
||||
@@ -269,37 +297,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}
|
||||
|
||||
|
||||
// add the current file to the thread being edited
|
||||
private _addCurrentFileAsStagingSelectionDuringFileChange() {
|
||||
const newModel = this._codeEditorService.getActiveCodeEditor()?.getModel() ?? null
|
||||
if (!newModel) { return }
|
||||
|
||||
const isCurrentlyFocusing = this.isCurrentlyFocusingMessage()
|
||||
if (isCurrentlyFocusing) return
|
||||
|
||||
// only add if the user hasn't sent a message yet
|
||||
if (this.getCurrentThread().messages.length !== 0) return
|
||||
|
||||
const newStagingSelection: StagingSelectionItem = {
|
||||
type: 'File',
|
||||
uri: newModel.uri,
|
||||
language: newModel.getLanguageId(),
|
||||
state: { wasAddedAsCurrentFile: true }
|
||||
}
|
||||
|
||||
const oldStagingSelections = this.getCurrentThreadState().stagingSelections || [];
|
||||
const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath)
|
||||
if (fileIsAlreadyHere) return
|
||||
|
||||
// remove all old selectons that are marked as `wasAddedAsCurrentFile`, and add new selection
|
||||
const newStagingSelections: StagingSelectionItem[] = [
|
||||
...oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile),
|
||||
newStagingSelection
|
||||
]
|
||||
this.setCurrentThreadState({ stagingSelections: newStagingSelections });
|
||||
|
||||
dangerousSetState = (newState: ThreadsState) => {
|
||||
this.state = newState
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
resetState = () => {
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // see constructor
|
||||
this.openNewThread()
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
|
||||
// !!! this is important for properly restoring URIs from storage
|
||||
// should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough
|
||||
@@ -369,36 +375,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
|
||||
|
||||
editUserMessageAndStreamResponse: IChatThreadService['editUserMessageAndStreamResponse'] = async ({ userMessage, messageIdx, threadId }) => {
|
||||
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
if (thread.messages?.[messageIdx]?.role !== 'user') {
|
||||
throw new Error(`Error: editing a message with role !=='user'`)
|
||||
}
|
||||
|
||||
// get prev and curr selections before clearing the message
|
||||
const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
|
||||
|
||||
// clear messages up to the index
|
||||
const slicedMessages = thread.messages.slice(0, messageIdx)
|
||||
this._setState({
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[thread.id]: {
|
||||
...thread,
|
||||
messages: slicedMessages
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
|
||||
// re-add the message and stream it
|
||||
this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
|
||||
|
||||
}
|
||||
|
||||
|
||||
private _currentModelSelectionProps = () => {
|
||||
// these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools)
|
||||
const featureName: FeatureName = 'Chat'
|
||||
@@ -414,7 +390,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
if (!messages) return false
|
||||
const lastMsg = messages[messages.length - 1]
|
||||
if (!lastMsg) return false
|
||||
if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) {
|
||||
|
||||
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
||||
this._editMessageInThread(threadId, messages.length - 1, tool)
|
||||
return true
|
||||
}
|
||||
@@ -431,9 +408,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
if (!thread) return // should never happen
|
||||
|
||||
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||
if (!(
|
||||
lastMsg.role === 'tool' && (lastMsg.type === 'tool_request')
|
||||
)) return // should never happen
|
||||
if (!(lastMsg.role === 'tool' && lastMsg.type === 'tool_request')) return // should never happen
|
||||
|
||||
const callThisToolFirst: ToolMessage<ToolName> = lastMsg
|
||||
|
||||
@@ -449,15 +424,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||
|
||||
let params: ToolCallParams[ToolName]
|
||||
if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) {
|
||||
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
||||
params = lastMsg.params
|
||||
}
|
||||
else return
|
||||
|
||||
const { name } = lastMsg
|
||||
const { name, id, rawParams } = lastMsg
|
||||
|
||||
const errorMessage = this.errMsgs.rejected
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
|
||||
this._setStreamState(threadId, {}, 'set')
|
||||
}
|
||||
|
||||
@@ -482,7 +457,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
|
||||
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
|
||||
if (toolCallSoFar) {
|
||||
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
@@ -505,69 +480,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {}
|
||||
|
||||
|
||||
|
||||
// system message
|
||||
private _generateSystemMessage = async (chatMode: ChatMode) => {
|
||||
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
|
||||
|
||||
const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
|
||||
const activeURI = this._editorService.activeEditor?.resource?.fsPath;
|
||||
|
||||
const directoryStr = await this._directoryStrService.getAllDirectoriesStr({
|
||||
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ? `...Directories string cut off, use tools to read more...`
|
||||
: `...Directories string cut off, ask user for more if necessary...`
|
||||
})
|
||||
|
||||
const runningTerminalIds = this._terminalToolService.listTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode })
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
private _generateLLMMessages = async (threadId: string) => {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return []
|
||||
|
||||
const chatMessages = deepClone(thread.messages)
|
||||
const llmChatMessages: LLMChatMessage[] = []
|
||||
|
||||
// merge tools into user message
|
||||
for (const c of chatMessages) {
|
||||
if (c.role === 'assistant') {
|
||||
// if called a tool, re-add its XML to the message
|
||||
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
|
||||
let content = c.displayContent
|
||||
if (c.toolCall) {
|
||||
content = `${content}\n\n${toolCallXMLStr(c.toolCall)}`
|
||||
}
|
||||
llmChatMessages.push({ role: c.role, content: content, anthropicReasoning: c.anthropicReasoning })
|
||||
}
|
||||
else if (c.role === 'user' || c.role === 'tool') {
|
||||
if (c.role === 'tool')
|
||||
c.content = `<${c.name}_result>\n${c.content}\n</${c.name}_result>`
|
||||
|
||||
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
|
||||
llmChatMessages.push({ role: 'user', content: c.content })
|
||||
else
|
||||
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
|
||||
|
||||
}
|
||||
else if (c.role === 'interrupted_streaming_tool') { // pass
|
||||
}
|
||||
else if (c.role === 'checkpoint') { // pass
|
||||
}
|
||||
else {
|
||||
throw new Error(`Role ${(c as any).role} not recognized.`)
|
||||
}
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
// returns true when the tool call is waiting for user approval
|
||||
private _runToolCall = async (
|
||||
threadId: string,
|
||||
toolName: ToolName,
|
||||
opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||
toolId: string,
|
||||
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
||||
|
||||
// compute these below
|
||||
@@ -582,18 +500,20 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, })
|
||||
return {}
|
||||
}
|
||||
// once validated, add checkpoint for edit
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
const toolRequiresApproval = toolNamesThatRequireApproval.has(toolName)
|
||||
if (toolRequiresApproval) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove
|
||||
|
||||
|
||||
const approvalType = approvalTypeOfToolName[toolName]
|
||||
if (approvalType) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType]
|
||||
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
@@ -603,9 +523,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
toolParams = opts.validatedParams
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 3. call the tool
|
||||
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
|
||||
let interrupted = false
|
||||
try {
|
||||
@@ -620,11 +542,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
}
|
||||
catch (error) {
|
||||
delete this._currentlyRunningToolInterruptor[threadId]
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
|
||||
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -633,12 +556,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
} catch (error) {
|
||||
const errorMessage = this.errMsgs.errWhenStringifying(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
delete this._currentlyRunningToolInterruptor[threadId]
|
||||
|
||||
return {}
|
||||
};
|
||||
@@ -659,6 +583,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' }
|
||||
}) {
|
||||
|
||||
|
||||
// above just defines helpers, below starts the actual function
|
||||
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
|
||||
|
||||
@@ -668,11 +593,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
let nMessagesSent = 0
|
||||
let shouldSendAnotherMessage = true
|
||||
let isRunningWhenEnd: IsRunningType = undefined
|
||||
let aborted = false
|
||||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params })
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
|
||||
if (interrupted) return
|
||||
}
|
||||
|
||||
@@ -683,81 +607,109 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
isRunningWhenEnd = undefined
|
||||
nMessagesSent += 1
|
||||
|
||||
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
|
||||
|
||||
// send llm message
|
||||
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
|
||||
const systemMessage = await this._generateSystemMessage(chatMode)
|
||||
const llmMessages = await this._generateLLMMessages(threadId)
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: systemMessage },
|
||||
...llmMessages
|
||||
]
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
messages,
|
||||
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
|
||||
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
|
||||
chatMessages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||
onText: ({ fullText, fullReasoning, toolCall }) => {
|
||||
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
|
||||
},
|
||||
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning })
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
||||
},
|
||||
onError: (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||
// add assistant's message to chat history, and clear selection
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
|
||||
this._setStreamState(threadId, { error }, 'set')
|
||||
resMessageIsDonePromise()
|
||||
},
|
||||
onAbort: () => {
|
||||
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
||||
resMessageIsDonePromise()
|
||||
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
||||
aborted = true
|
||||
},
|
||||
chatMode
|
||||
})
|
||||
|
||||
// should never happen, just for safety
|
||||
if (llmCancelToken === null) {
|
||||
this._setStreamState(threadId, {
|
||||
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
|
||||
}, 'set')
|
||||
break
|
||||
}
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
if (aborted) { return }
|
||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
||||
let aborted = false
|
||||
let shouldRetry = true
|
||||
let nAttempts = 0
|
||||
while (shouldRetry) {
|
||||
shouldRetry = false
|
||||
|
||||
// call tool if there is one
|
||||
const tool: RawToolCallObj | undefined = toolCall
|
||||
if (tool) {
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams })
|
||||
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
|
||||
|
||||
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
|
||||
// just detect tool interruption which is the same as chat interruption right now
|
||||
if (interrupted) { return }
|
||||
// send llm message
|
||||
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
|
||||
|
||||
if (awaitingUserApproval) {
|
||||
isRunningWhenEnd = 'awaiting_user'
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
messages: messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||
separateSystemMessage: separateSystemMessage,
|
||||
onText: ({ fullText, fullReasoning, toolCall }) => {
|
||||
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
|
||||
},
|
||||
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
||||
|
||||
},
|
||||
onError: async (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
if (nAttempts < CHAT_RETRIES) {
|
||||
nAttempts += 1
|
||||
shouldRetry = true
|
||||
await timeout(RETRY_DELAY)
|
||||
resMessageIsDonePromise()
|
||||
}
|
||||
else {
|
||||
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||
// add assistant's message to chat history, and clear selection
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
this._setStreamState(threadId, { error }, 'set')
|
||||
resMessageIsDonePromise()
|
||||
}
|
||||
},
|
||||
onAbort: () => {
|
||||
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
||||
aborted = true
|
||||
resMessageIsDonePromise()
|
||||
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
||||
},
|
||||
})
|
||||
|
||||
// should never happen, just for safety
|
||||
if (llmCancelToken === null) {
|
||||
this._setStreamState(threadId, {
|
||||
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
|
||||
}, 'set')
|
||||
break
|
||||
}
|
||||
else {
|
||||
shouldSendAnotherMessage = true
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
||||
|
||||
// this is a complete hack to make it so if an error loop was aborted, we stop (because onAbort does not get called if error happens instantly)
|
||||
// maybe we should remove all the abort stuff and just make it so that we only go by state?
|
||||
if (!this.streamState[threadId]?.isRunning) { return }
|
||||
|
||||
if (aborted) { return }
|
||||
if (shouldRetry) { continue }
|
||||
|
||||
// call tool if there is one
|
||||
const tool: RawToolCallObj | undefined = toolCall
|
||||
if (tool) {
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams })
|
||||
|
||||
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
|
||||
// just detect tool interruption which is the same as chat interruption right now
|
||||
if (!this.streamState[threadId]?.isRunning) { return }
|
||||
if (aborted) { return }
|
||||
if (interrupted) { return }
|
||||
|
||||
if (awaitingUserApproval) {
|
||||
console.log('awaiting...')
|
||||
isRunningWhenEnd = 'awaiting_user'
|
||||
}
|
||||
else {
|
||||
shouldSendAnotherMessage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // end while
|
||||
|
||||
} // end while (attempts)
|
||||
} // end while (send message)
|
||||
|
||||
// if awaiting user approval, keep isRunning true, else end isRunning
|
||||
this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge')
|
||||
@@ -967,15 +919,14 @@ A,B,C are all files.
|
||||
x means a checkpoint where the file changed.
|
||||
|
||||
A B C D E F G H I
|
||||
x x x x x x x x x
|
||||
| | | | | | | | |
|
||||
x | | | | | | | x
|
||||
---x-|-|-|-x-|-x-|----- <-- to
|
||||
x | | | | | x
|
||||
| | x x |
|
||||
| | | |
|
||||
-------x-|---x-x------- <-- from
|
||||
x
|
||||
x x x x x x <-- you can't always go up to find the "before" version; sometimes you need to go down
|
||||
| | | | | | x
|
||||
--x-|-|-|-x---x-|----- <-- to
|
||||
| | | | x x
|
||||
| | x x |
|
||||
| | | |
|
||||
----x-|---x-x------- <-- from
|
||||
x
|
||||
|
||||
We need to revert anything that happened between to+1 and from.
|
||||
**We do this by finding the last x from 0...`to` for each file and applying those contents.**
|
||||
@@ -983,9 +934,19 @@ We only need to do it for files that were edited since `to`, ie files between to
|
||||
*/
|
||||
if (toIdx < fromIdx) {
|
||||
const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: toIdx + 1, hiIdx: fromIdx })
|
||||
|
||||
const idxes = function* () {
|
||||
for (let k = toIdx; k >= 0; k -= 1) { // first go up
|
||||
yield k
|
||||
}
|
||||
for (let k = toIdx + 1; k < thread.messages.length; k += 1) { // then go down
|
||||
yield k
|
||||
}
|
||||
}
|
||||
|
||||
for (const fsPath in lastIdxOfURI) {
|
||||
// apply lowest down content for each uri (or original if not found)
|
||||
for (let k = toIdx; k >= 0; k -= 1) {
|
||||
// find the first instance of this file starting at toIdx (go up to latest file; if there is none, go down)
|
||||
for (const k of idxes()) {
|
||||
const message = thread.messages[k]
|
||||
if (message.role !== 'checkpoint') continue
|
||||
const res = this._getCheckpointInfo(message, fsPath, { includeUserModifiedChanges: jumpToUserModified })
|
||||
@@ -1001,16 +962,16 @@ We only need to do it for files that were edited since `to`, ie files between to
|
||||
/*
|
||||
if redoing
|
||||
|
||||
A B C D E F G H I
|
||||
x x x x x x x x x
|
||||
| | | | | | | | |
|
||||
x | | | | | | | x
|
||||
---x-|-|-|-x-|-x-|----- <-- from
|
||||
x | | | | | x
|
||||
| | x x |
|
||||
| | | |
|
||||
-------x-|---x-x------- <-- to
|
||||
x
|
||||
A B C D E F G H I J
|
||||
x x x x x x x
|
||||
| | | | | | x x x
|
||||
--x-|-|-|-x---x-|-|--- <-- from
|
||||
| | | | x x
|
||||
| | x x |
|
||||
| | | |
|
||||
----x-|---x-x-----|--- <-- to
|
||||
x x
|
||||
|
||||
|
||||
We need to apply latest change for anything that happened between from+1 and to.
|
||||
We only need to do it for files that were edited since `from`, ie files between from+1...to.
|
||||
@@ -1026,7 +987,6 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
if (!res) continue
|
||||
const { voidFileSnapshot } = res
|
||||
if (!voidFileSnapshot) continue
|
||||
|
||||
this._editCodeService.restoreVoidFileSnapshot(URI.file(fsPath), voidFileSnapshot)
|
||||
break
|
||||
}
|
||||
@@ -1075,27 +1035,28 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
})
|
||||
}
|
||||
|
||||
async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
|
||||
dismissStreamError(threadId: string): void {
|
||||
this._setStreamState(threadId, { error: undefined }, 'merge')
|
||||
}
|
||||
|
||||
|
||||
private async _addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken // currently streaming LLM on this thread
|
||||
if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning === 'LLM') {
|
||||
// if about to call the other LLM, just wait for it by stopping right now
|
||||
if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning) {
|
||||
// if about to call the other LLM, just wait for and stop now
|
||||
return
|
||||
}
|
||||
// stop it (this simply resolves the promise to free up space)
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
|
||||
|
||||
|
||||
// add dummy before this message to keep checkpoint before user message idea consistent
|
||||
if (thread.messages.length === 0) {
|
||||
this._addUserCheckpoint({ threadId })
|
||||
}
|
||||
|
||||
|
||||
const { chatMode } = this._settingsService.state.globalSettings
|
||||
|
||||
// add user's message to chat history
|
||||
@@ -1115,11 +1076,61 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
)
|
||||
}
|
||||
|
||||
dismissStreamError(threadId: string): void {
|
||||
this._setStreamState(threadId, { error: undefined }, 'merge')
|
||||
|
||||
async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
|
||||
const thread = this.state.allThreads[threadId];
|
||||
if (!thread) return
|
||||
|
||||
// if there's a current checkpoint, delete all messages after it
|
||||
if (thread.state.currCheckpointIdx !== null) {
|
||||
const checkpointIdx = thread.state.currCheckpointIdx;
|
||||
const newMessages = thread.messages.slice(0, checkpointIdx + 1);
|
||||
|
||||
// Update the thread with truncated messages
|
||||
const newThreads = {
|
||||
...this.state.allThreads,
|
||||
[threadId]: {
|
||||
...thread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: newMessages,
|
||||
}
|
||||
};
|
||||
this._storeAllThreads(newThreads);
|
||||
this._setState({ allThreads: newThreads }, true);
|
||||
}
|
||||
|
||||
// Now call the original method to add the user message and stream the response
|
||||
await this._addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId });
|
||||
|
||||
}
|
||||
|
||||
editUserMessageAndStreamResponse: IChatThreadService['editUserMessageAndStreamResponse'] = async ({ userMessage, messageIdx, threadId }) => {
|
||||
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
if (thread.messages?.[messageIdx]?.role !== 'user') {
|
||||
throw new Error(`Error: editing a message with role !=='user'`)
|
||||
}
|
||||
|
||||
// get prev and curr selections before clearing the message
|
||||
const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
|
||||
|
||||
// clear messages up to the index
|
||||
const slicedMessages = thread.messages.slice(0, messageIdx)
|
||||
this._setState({
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[thread.id]: {
|
||||
...thread,
|
||||
messages: slicedMessages
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
|
||||
// re-add the message and stream it
|
||||
this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
|
||||
}
|
||||
|
||||
// ---------- the rest ----------
|
||||
|
||||
@@ -1214,8 +1225,9 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
// else search codebase for `target`
|
||||
let uris: URI[] = []
|
||||
try {
|
||||
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, searchInFolder: null, pageNumber: 0 })
|
||||
uris = result.uris
|
||||
const { result } = await this._toolsService.callTool['search_pathnames_only']({ query: target, includePattern: null, pageNumber: 0 })
|
||||
const { uris: uris_ } = await result
|
||||
uris = uris_
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
@@ -1413,25 +1425,9 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId]!.messages.length === 0) {
|
||||
|
||||
// switch to the thread
|
||||
// switch to the existing empty thread and exit
|
||||
this.switchToThread(threadId)
|
||||
|
||||
// add the current file as a staging selection
|
||||
const model = this._codeEditorService.getActiveCodeEditor()?.getModel()
|
||||
if (model) {
|
||||
this._setThreadState(this.state.currentThreadId, {
|
||||
stagingSelections: [{
|
||||
type: 'File',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
state: {
|
||||
wasAddedAsCurrentFile: true
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
@@ -1447,6 +1443,35 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
|
||||
|
||||
deleteThread(threadId: string): void {
|
||||
const { allThreads: currentThreads } = this.state
|
||||
|
||||
// delete the thread
|
||||
const newThreads = { ...currentThreads };
|
||||
delete newThreads[threadId];
|
||||
|
||||
// store the updated threads
|
||||
this._storeAllThreads(newThreads);
|
||||
this._setState({ ...this.state, allThreads: newThreads }, true)
|
||||
}
|
||||
|
||||
duplicateThread(threadId: string) {
|
||||
const { allThreads: currentThreads } = this.state
|
||||
const threadToDuplicate = currentThreads[threadId]
|
||||
if (!threadToDuplicate) return
|
||||
const newThread = {
|
||||
...deepClone(threadToDuplicate),
|
||||
id: generateUuid(),
|
||||
}
|
||||
const newThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread,
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true)
|
||||
}
|
||||
|
||||
|
||||
private _addMessageToThread(threadId: string, message: ChatMessage) {
|
||||
const { allThreads } = this.state
|
||||
const oldThread = allThreads[threadId]
|
||||
|
||||
@@ -0,0 +1,648 @@
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
|
||||
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
|
||||
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
|
||||
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
|
||||
import { IDirectoryStrService } from './directoryStrService.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
|
||||
|
||||
|
||||
type SimpleLLMMessage = {
|
||||
role: 'tool';
|
||||
content: string;
|
||||
id: string;
|
||||
name: ToolName;
|
||||
rawParams: RawToolParamsObj;
|
||||
} | {
|
||||
role: 'user';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant';
|
||||
content: string;
|
||||
anthropicReasoning: AnthropicReasoning[] | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const TRIM_TO_LEN = 60
|
||||
|
||||
|
||||
|
||||
|
||||
// convert messages as if about to send to openai
|
||||
/*
|
||||
reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
|
||||
openai MESSAGE (role=assistant):
|
||||
"tool_calls":[{
|
||||
"type": "function",
|
||||
"id": "call_12345xyz",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
|
||||
}]
|
||||
|
||||
openai RESPONSE (role=user):
|
||||
{ "role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result) }
|
||||
|
||||
also see
|
||||
openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
*/
|
||||
|
||||
|
||||
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => {
|
||||
|
||||
const newMessages: OpenAILLMChatMessage[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') {
|
||||
newMessages.push(currMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
// edit previous assistant message to have called the tool
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
prevMsg.tool_calls = [{
|
||||
type: 'function',
|
||||
id: currMsg.id,
|
||||
function: {
|
||||
name: currMsg.name,
|
||||
arguments: JSON.stringify(currMsg.rawParams)
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// add the tool
|
||||
newMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: currMsg.id,
|
||||
content: currMsg.content,
|
||||
})
|
||||
}
|
||||
return newMessages
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// convert messages as if about to send to anthropic
|
||||
/*
|
||||
https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
|
||||
anthropic MESSAGE (role=assistant):
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
|
||||
}, {
|
||||
"type": "tool_use",
|
||||
"id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"name": "get_weather",
|
||||
"input": { "location": "San Francisco, CA", "unit": "celsius" }
|
||||
}]
|
||||
anthropic RESPONSE (role=user):
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"content": "15 degrees"
|
||||
}]
|
||||
|
||||
|
||||
Converts:
|
||||
assistant: ...content
|
||||
tool: (id, name, params)
|
||||
->
|
||||
assistant: ...content, call(name, id, params)
|
||||
user: ...content, result(id, content)
|
||||
*/
|
||||
|
||||
|
||||
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages;
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
|
||||
// add anthropic reasoning
|
||||
if (currMsg.role === 'assistant') {
|
||||
if (currMsg.anthropicReasoning && supportsAnthropicReasoning) {
|
||||
const content = currMsg.content
|
||||
newMessages[i] = {
|
||||
role: 'assistant',
|
||||
content: content ? [...currMsg.anthropicReasoning, { type: 'text' as const, text: content }] : currMsg.anthropicReasoning
|
||||
}
|
||||
}
|
||||
else {
|
||||
newMessages[i] = {
|
||||
role: 'assistant',
|
||||
content: currMsg.content,
|
||||
// strip away anthropicReasoning
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (currMsg.role === 'user') {
|
||||
newMessages[i] = {
|
||||
role: 'user',
|
||||
content: currMsg.content,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (currMsg.role === 'tool') {
|
||||
// add anthropic tools
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
|
||||
// make it so the assistant called the tool
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
|
||||
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: currMsg.rawParams })
|
||||
}
|
||||
|
||||
// turn each tool into a user message with tool results at the end
|
||||
newMessages[i] = {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// we just removed the tools
|
||||
return newMessages as AnthropicLLMChatMessage[]
|
||||
}
|
||||
|
||||
|
||||
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
|
||||
const llmChatMessages: LLMChatMessage[] = [];
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
|
||||
const c = messages[i]
|
||||
const next = 0 <= i + 1 && i + 1 <= messages.length - 1 ? messages[i + 1] : null
|
||||
|
||||
if (c.role === 'assistant') {
|
||||
// if called a tool (message after it), re-add its XML to the message
|
||||
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
|
||||
let content: LLMChatMessage['content'] = c.content
|
||||
if (next?.role === 'tool') {
|
||||
content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}`
|
||||
}
|
||||
|
||||
// anthropic reasoning
|
||||
if (c.anthropicReasoning && supportsAnthropicReasoning) {
|
||||
content = content ? [...c.anthropicReasoning, { type: 'text' as const, text: content }] : c.anthropicReasoning
|
||||
}
|
||||
llmChatMessages.push({
|
||||
role: 'assistant',
|
||||
content
|
||||
})
|
||||
}
|
||||
// add user or tool to the previous user message
|
||||
else if (c.role === 'user' || c.role === 'tool') {
|
||||
if (c.role === 'tool')
|
||||
c.content = `<${c.name}_result>\n${c.content}\n</${c.name}_result>`
|
||||
|
||||
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
|
||||
llmChatMessages.push({
|
||||
role: 'user',
|
||||
content: c.content
|
||||
})
|
||||
else
|
||||
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
|
||||
}
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
|
||||
const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
const llmChatMessages: LLMChatMessage[] = []
|
||||
if (!specialToolFormat) { // XML tool behavior
|
||||
return prepareMessages_XML_tools(messages, supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'anthropic-style') {
|
||||
return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'openai-style') {
|
||||
return prepareMessages_openai_tools(messages)
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
// --- CHAT ---
|
||||
|
||||
const prepareMessages = ({
|
||||
messages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
}: {
|
||||
messages: SimpleLLMMessage[],
|
||||
systemMessage: string,
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
specialToolFormat: 'openai-style' | 'anthropic-style' | undefined,
|
||||
supportsAnthropicReasoning: boolean,
|
||||
contextWindow: number,
|
||||
maxOutputTokens: number | null | undefined,
|
||||
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
|
||||
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
|
||||
|
||||
// ================ trim ================
|
||||
|
||||
messages = deepClone(messages)
|
||||
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
|
||||
|
||||
// ================ fit into context ================
|
||||
|
||||
// the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES
|
||||
const alreadyTrimmedIdxes = new Set<number>()
|
||||
const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => {
|
||||
const base = message.content.length
|
||||
|
||||
let multiplier: number
|
||||
multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases
|
||||
if (message.role === 'user') {
|
||||
multiplier *= 1
|
||||
}
|
||||
else {
|
||||
multiplier *= 10 // llm tokens are far less valuable than user tokens
|
||||
}
|
||||
// 1st message, last 3 msgs, any already modified message should be low in weight
|
||||
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
|
||||
multiplier *= .05
|
||||
}
|
||||
return base * multiplier
|
||||
}
|
||||
|
||||
const _findLargestByWeight = (messages: SimpleLLMMessage[]) => {
|
||||
let largestIndex = -1
|
||||
let largestWeight = -Infinity
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const m = messages[i]
|
||||
const w = weight(m, messages, i)
|
||||
if (w > largestWeight) {
|
||||
largestWeight = w
|
||||
largestIndex = i
|
||||
}
|
||||
}
|
||||
return largestIndex
|
||||
}
|
||||
|
||||
let totalLen = 0
|
||||
for (const m of messages) { totalLen += m.content.length }
|
||||
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
|
||||
|
||||
// <----------------------------------------->
|
||||
// 0 | | |
|
||||
// | contextWindow |
|
||||
// contextWindow - maxOut|putTokens
|
||||
// totalLen
|
||||
let remainingCharsToTrim = charsNeedToTrim
|
||||
let i = 0
|
||||
|
||||
while (remainingCharsToTrim > 0) {
|
||||
i += 1
|
||||
if (i > 100) break
|
||||
|
||||
const trimIdx = _findLargestByWeight(messages)
|
||||
const m = messages[trimIdx]
|
||||
|
||||
// if can finish here, do
|
||||
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
|
||||
if (numCharsWillTrim > remainingCharsToTrim) {
|
||||
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim()
|
||||
break
|
||||
}
|
||||
|
||||
remainingCharsToTrim -= numCharsWillTrim
|
||||
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
|
||||
alreadyTrimmedIdxes.add(trimIdx)
|
||||
}
|
||||
|
||||
// ================ tools and anthropicReasoning ================
|
||||
const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning)
|
||||
|
||||
// ================ system message concat ================
|
||||
|
||||
// find system messages and concatenate them
|
||||
const newSystemMessage = aiInstructions ?
|
||||
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
|
||||
: systemMessage
|
||||
|
||||
let separateSystemMessageStr: string | undefined = undefined
|
||||
|
||||
// if supports system message
|
||||
if (supportsSystemMessage) {
|
||||
if (supportsSystemMessage === 'separated')
|
||||
separateSystemMessageStr = newSystemMessage
|
||||
else if (supportsSystemMessage === 'system-role')
|
||||
llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message
|
||||
else if (supportsSystemMessage === 'developer-role')
|
||||
llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message
|
||||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
const newFirstMessage = {
|
||||
role: 'user',
|
||||
content: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
|
||||
} as const
|
||||
llmMessages.splice(0, 1) // delete first message
|
||||
llmMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
|
||||
|
||||
// ================ no empty message ================
|
||||
for (const currMsg of llmMessages) {
|
||||
if (currMsg.role === 'tool') continue
|
||||
|
||||
// if content is a string, replace string with empty msg
|
||||
if (typeof currMsg.content === 'string')
|
||||
currMsg.content = currMsg.content || EMPTY_MESSAGE
|
||||
else {
|
||||
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
|
||||
for (const c of currMsg.content) {
|
||||
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
|
||||
}
|
||||
if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages: llmMessages,
|
||||
separateSystemMessage: separateSystemMessageStr,
|
||||
} as const
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export interface IConvertToLLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }
|
||||
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
|
||||
prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] }
|
||||
|
||||
}
|
||||
|
||||
export const IConvertToLLMMessageService = createDecorator<IConvertToLLMMessageService>('ConvertToLLMMessageService');
|
||||
|
||||
|
||||
class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMessageService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IDirectoryStrService private readonly directoryStrService: IDirectoryStrService,
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
// Read .voidrules files from workspace folders
|
||||
private _getVoidRulesFileContents(): string {
|
||||
try {
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
|
||||
let voidRules = '';
|
||||
for (const folder of workspaceFolders) {
|
||||
const uri = URI.joinPath(folder.uri, '.voidrules')
|
||||
const { model } = this.voidModelService.getModel(uri)
|
||||
if (!model) continue
|
||||
voidRules += model.getValue() + '\n\n';
|
||||
}
|
||||
return voidRules.trim();
|
||||
}
|
||||
catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// Get combined AI instructions from settings and .voidrules files
|
||||
private _getCombinedAIInstructions(): string {
|
||||
const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions;
|
||||
const voidRulesFileContent = this._getVoidRulesFileContents();
|
||||
|
||||
const ans: string[] = []
|
||||
if (globalAIInstructions) ans.push(globalAIInstructions)
|
||||
if (voidRulesFileContent) ans.push(voidRulesFileContent)
|
||||
return ans.join('\n\n')
|
||||
}
|
||||
|
||||
|
||||
// system message
|
||||
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
|
||||
|
||||
const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
|
||||
const activeURI = this.editorService.activeEditor?.resource?.fsPath;
|
||||
|
||||
const directoryStr = await this.directoryStrService.getAllDirectoriesStr({
|
||||
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ?
|
||||
`...Directories string cut off, use tools to read more...`
|
||||
: `...Directories string cut off, ask user for more if necessary...`
|
||||
})
|
||||
const includeXMLToolDefinitions = !specialToolFormat
|
||||
|
||||
const runningTerminalIds = this.terminalToolService.listTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions })
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// --- LLM Chat messages ---
|
||||
|
||||
private _chatMessagesToSimpleMessages(chatMessages: ChatMessage[]): SimpleLLMMessage[] {
|
||||
const simpleLLMMessages: SimpleLLMMessage[] = []
|
||||
|
||||
for (const m of chatMessages) {
|
||||
if (m.role === 'checkpoint') continue
|
||||
if (m.role === 'interrupted_streaming_tool') continue
|
||||
if (m.role === 'assistant') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.displayContent,
|
||||
anthropicReasoning: m.anthropicReasoning,
|
||||
})
|
||||
}
|
||||
else if (m.role === 'tool') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
name: m.name,
|
||||
id: m.id,
|
||||
rawParams: m.rawParams,
|
||||
})
|
||||
}
|
||||
else if (m.role === 'user') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})
|
||||
}
|
||||
}
|
||||
return simpleLLMMessages
|
||||
}
|
||||
|
||||
prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => {
|
||||
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
|
||||
const { providerName, modelName } = modelSelection
|
||||
const {
|
||||
specialToolFormat,
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
|
||||
// Get combined AI instructions
|
||||
const aiInstructions = this._getCombinedAIInstructions();
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
|
||||
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
|
||||
|
||||
const { messages, separateSystemMessage } = prepareMessages({
|
||||
messages: simpleMessages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
}
|
||||
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
|
||||
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
|
||||
const { providerName, modelName } = modelSelection
|
||||
const {
|
||||
specialToolFormat,
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
|
||||
// Get combined AI instructions
|
||||
const aiInstructions = this._getCombinedAIInstructions();
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
|
||||
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
|
||||
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
|
||||
|
||||
const { messages, separateSystemMessage } = prepareMessages({
|
||||
messages: llmMessages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
}
|
||||
|
||||
|
||||
// --- FIM ---
|
||||
|
||||
prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages }) => {
|
||||
// Get combined AI instructions with the provided aiInstructions as the base
|
||||
const combinedInstructions = this._getCombinedAIInstructions();
|
||||
|
||||
let prefix = `\
|
||||
${!combinedInstructions ? '' : `\
|
||||
// Instructions:
|
||||
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
|
||||
${combinedInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
|
||||
|
||||
${messages.prefix}`
|
||||
|
||||
const suffix = messages.suffix
|
||||
const stopTokens = messages.stopTokens
|
||||
return { prefix, suffix, stopTokens }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IConvertToLLMMessageService, ConvertToLLMMessageService, InstantiationType.Eager);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Gemini has this, but they're openai-compat so we don't need to implement this
|
||||
gemini request:
|
||||
{ "role": "assistant",
|
||||
"content": null,
|
||||
"function_call": {
|
||||
"name": "get_weather",
|
||||
"arguments": {
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gemini response:
|
||||
{ "role": "assistant",
|
||||
"function_response": {
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"temperature": "15°C",
|
||||
"condition": "Cloudy"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
|
||||
class ConvertContribWorkbenchContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.void.convertcontrib'
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
||||
@IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService,
|
||||
) {
|
||||
super()
|
||||
|
||||
const initializeURI = (uri: URI) => {
|
||||
this.workspaceContext.getWorkspace()
|
||||
const voidRulesURI = URI.joinPath(uri, '.voidrules')
|
||||
this.voidModelService.initializeModel(voidRulesURI)
|
||||
}
|
||||
|
||||
// call
|
||||
this._register(this.workspaceContext.onDidChangeWorkspaceFolders((e) => {
|
||||
[...e.changed, ...e.added].forEach(w => { initializeURI(w.uri) })
|
||||
}))
|
||||
this.workspaceContext.getWorkspace().folders.forEach(w => { initializeURI(w.uri) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registerWorkbenchContribution2(ConvertContribWorkbenchContribution.ID, ConvertContribWorkbenchContribution, WorkbenchPhase.BlockRestore);
|
||||
@@ -10,11 +10,10 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE } from './toolsService.js';
|
||||
import { IExplorerService } from '../../files/browser/files.js';
|
||||
import { SortOrder } from '../../files/common/files.js';
|
||||
import { ExplorerItem } from '../../files/common/explorerModel.js';
|
||||
import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
|
||||
|
||||
const MAX_FILES_TOTAL = 300;
|
||||
@@ -111,14 +110,14 @@ export const computeDirectoryTree1Deep = async (
|
||||
|
||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.rootURI} is not a directory`;
|
||||
return `Error: ${params.uri} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) { // is first page
|
||||
output += `${params.rootURI.fsPath}\n`;
|
||||
output += `${params.uri.fsPath}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
@@ -327,7 +326,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
|
||||
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
|
||||
const eRoot = this.explorerService.findClosest(uri)
|
||||
if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`)
|
||||
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
|
||||
|
||||
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
|
||||
@@ -417,10 +416,9 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log('cutoff!!!!!!!', str, cutOffMessage)
|
||||
|
||||
if (cutOff) {
|
||||
return `${str}\n${cutOffMessage}`
|
||||
return `${str.trimEnd()}\n${cutOffMessage}`
|
||||
}
|
||||
|
||||
return str
|
||||
|
||||
@@ -31,14 +31,11 @@ import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
|
||||
import { QuickEditPropsType } from './quickEditActions.js';
|
||||
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
|
||||
import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
import { INotificationService, } from '../../../../platform/notification/common/notification.js';
|
||||
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
|
||||
import { Emitter } from '../../../../base/common/event.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
|
||||
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
@@ -47,6 +44,9 @@ import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
|
||||
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
// import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
@@ -198,10 +198,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
@IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService,
|
||||
@IMetricsService private readonly _metricsService: IMetricsService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
// @ICommandService private readonly _commandService: ICommandService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
// @IFileService private readonly _fileService: IFileService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -256,29 +257,45 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._realignAllDiffAreasLines(uri, change.text, change.range)
|
||||
}
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
|
||||
// if diffarea has no diffs after a user edit, delete it
|
||||
const diffAreasToDelete: DiffZone[] = []
|
||||
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] ?? []) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid] ?? null
|
||||
const shouldDelete = diffArea?.type === 'DiffZone' && Object.keys(diffArea._diffOfId).length === 0
|
||||
if (shouldDelete) {
|
||||
diffAreasToDelete.push(diffArea)
|
||||
}
|
||||
}
|
||||
if (diffAreasToDelete.length !== 0) {
|
||||
const { onFinishEdit } = this._addToHistory(uri)
|
||||
diffAreasToDelete.forEach(da => this._deleteDiffZone(da))
|
||||
onFinishEdit()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private _notifyError = (e: Parameters<OnError>[0]) => {
|
||||
const details = errorDetails(e.fullError)
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: `Void Error: ${e.message}`,
|
||||
actions: {
|
||||
secondary: [{
|
||||
id: 'void.onerror.opensettings',
|
||||
enabled: true,
|
||||
label: `Open Void's settings`,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
|
||||
}]
|
||||
},
|
||||
source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
|
||||
})
|
||||
}
|
||||
// private _notifyError = (e: Parameters<OnError>[0]) => {
|
||||
// const details = errorDetails(e.fullError)
|
||||
// this._notificationService.notify({
|
||||
// severity: Severity.Warning,
|
||||
// message: `Void Error: ${e.message}`,
|
||||
// actions: {
|
||||
// secondary: [{
|
||||
// id: 'void.onerror.opensettings',
|
||||
// enabled: true,
|
||||
// label: `Open Void's settings`,
|
||||
// tooltip: '',
|
||||
// class: undefined,
|
||||
// run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
|
||||
// }]
|
||||
// },
|
||||
// source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
|
||||
// })
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@@ -1233,7 +1250,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
_removeStylesFns: new Set(),
|
||||
}
|
||||
|
||||
console.log('FIRING START STREAMING IN DIFFZONE!!!')
|
||||
const diffZone = this._addDiffArea(adding)
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
@@ -1267,6 +1283,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
|
||||
|
||||
const { from, } = opts
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
@@ -1300,12 +1320,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n')
|
||||
const language = model.getLanguageId()
|
||||
let messages: LLMChatMessage[]
|
||||
let separateSystemMessage: string | undefined
|
||||
if (from === 'ClickApply') {
|
||||
const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language })
|
||||
messages = [
|
||||
{ role: 'system', content: rewriteCode_systemMessage, },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: rewriteCode_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }), }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
messages = a
|
||||
separateSystemMessage = b
|
||||
}
|
||||
else if (from === 'QuickEdit') {
|
||||
if (!ctrlKZoneIfQuickEdit) return
|
||||
@@ -1316,11 +1340,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
|
||||
const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine })
|
||||
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language })
|
||||
// type: 'messages',
|
||||
messages = [
|
||||
{ role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
|
||||
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }),
|
||||
simpleMessages: [{ role: 'user', content: userContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
messages = a
|
||||
separateSystemMessage = b
|
||||
|
||||
}
|
||||
else { throw new Error(`featureName ${from} is invalid`) }
|
||||
|
||||
@@ -1363,7 +1392,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
// throws
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
this._notifyError(e)
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError
|
||||
@@ -1384,10 +1413,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
const latestStreamLocationMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runWriteover = async () => {
|
||||
let shouldSendAnotherMessage = true
|
||||
@@ -1410,6 +1435,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText: fullText_ } = params
|
||||
@@ -1485,6 +1511,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
|
||||
const { from, applyStr, } = opts
|
||||
const featureName: FeatureName = 'Apply'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
@@ -1498,10 +1527,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// build messages - ask LLM to generate search/replace block text
|
||||
const originalFileCode = model.getValue(EndOfLinePreference.LF)
|
||||
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: searchReplace_systemMessage },
|
||||
{ role: 'user', content: userMessageContent },
|
||||
]
|
||||
|
||||
const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: searchReplace_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: userMessageContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
|
||||
// if URI is already streaming, return (should never happen, caller is responsible for checking)
|
||||
if (this._uriIsStreaming(uri)) return
|
||||
@@ -1545,20 +1577,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
|
||||
|
||||
const errContentOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => {
|
||||
const errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => {
|
||||
|
||||
const descStr = str === `Not found` ?
|
||||
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: str === `Not unique` ?
|
||||
`The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: ``
|
||||
: str === 'Has overlap' ?
|
||||
`The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: ``
|
||||
|
||||
// string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has
|
||||
// const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n')
|
||||
// const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : ''
|
||||
// const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : ''
|
||||
// const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}`
|
||||
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error.'
|
||||
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
|
||||
const errMsg = `${descStr}\n${soFarStr}`
|
||||
return errMsg
|
||||
|
||||
@@ -1577,7 +1611,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
this._notifyError(e)
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError || new Error(e.message) // throw error h
|
||||
@@ -1593,11 +1627,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
|
||||
diffZone._streamState.line = 1
|
||||
|
||||
const featureName: FeatureName = 'Apply'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const N_RETRIES = 2
|
||||
const N_RETRIES = 4
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runSearchReplace = async () => {
|
||||
@@ -1628,6 +1658,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
@@ -1670,19 +1701,27 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
|
||||
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
|
||||
|
||||
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
|
||||
// if error
|
||||
if (typeof originalBounds === 'string') {
|
||||
// Check for overlap with existing modified ranges
|
||||
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
|
||||
const [existingStart, existingEnd] = trackingZone.metadata.originalBounds;
|
||||
const hasNoOverlap = endLine < existingStart || startLine > existingEnd
|
||||
return !hasNoOverlap
|
||||
});
|
||||
|
||||
if (typeof originalBounds === 'string' || hasOverlap) {
|
||||
const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const
|
||||
|
||||
console.log('--------------Error finding text in code:')
|
||||
console.log('originalFileCode', { originalFileCode })
|
||||
console.log('fullText', { fullText })
|
||||
console.log('error:', originalBounds)
|
||||
console.log('error:', errorMessage)
|
||||
console.log('block.orig:', block.orig)
|
||||
console.log('---------')
|
||||
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
|
||||
const content = errContentOfInvalidStr(errorMessage, block.orig)
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: content } // user explanation of what's wrong
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
background-color: var(--vscode-void-sweepIdxBG);
|
||||
}
|
||||
|
||||
@@ -23,10 +23,30 @@
|
||||
background-color: var(--vscode-void-redBG);
|
||||
}
|
||||
|
||||
.void-watermark-button {
|
||||
margin: 8px 0;
|
||||
|
||||
/* Renamed from void-watermark-button to void-openfolder-button */
|
||||
.void-openfolder-button {
|
||||
padding: 8px 20px;
|
||||
background-color: #3b82f6;
|
||||
background-color: #306dce;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.void-openfolder-button:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
.void-openfolder-button:active {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Added for Open SSH button with slightly darker color */
|
||||
.void-openssh-button {
|
||||
padding: 8px 20px;
|
||||
background-color: #656565; /* Slightly darker than the #5a5a5a in the TS file */
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
@@ -35,43 +55,41 @@
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.void-watermark-button:hover {
|
||||
background-color: #2563eb;
|
||||
.void-openssh-button:hover {
|
||||
background-color: #474747; /* Darker on hover */
|
||||
}
|
||||
.void-watermark-button:active {
|
||||
background-color: #2563eb;
|
||||
.void-openssh-button:active {
|
||||
background-color: #474747;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.void-settings-watermark-button {
|
||||
margin: 8px 0;
|
||||
padding: 8px 20px;
|
||||
background-color: var(--vscode-input-background);
|
||||
margin: 8px 0;
|
||||
padding: 8px 20px;
|
||||
background-color: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.void-settings-watermark-button:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.void-settings-watermark-button:active {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.void-link {
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.void-link:hover {
|
||||
opacity: 80%;
|
||||
}
|
||||
@@ -86,7 +104,8 @@
|
||||
.void-scope,
|
||||
.void-scope * {
|
||||
scrollbar-width: thin !important;
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important;
|
||||
/* For Firefox */
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar,
|
||||
@@ -133,13 +152,16 @@
|
||||
background-color: var(--vscode-editor-background);
|
||||
--scrollbar-vertical-width: 14px;
|
||||
--scrollbar-horizontal-height: 6px;
|
||||
overflow: auto; /* Ensure scrollbars are shown when needed */
|
||||
overflow: auto;
|
||||
/* Ensure scrollbars are shown when needed */
|
||||
}
|
||||
|
||||
.void-scrollable-element,
|
||||
.void-scrollable-element * {
|
||||
scrollbar-width: thin !important; /* For Firefox */
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
|
||||
scrollbar-width: thin !important;
|
||||
/* For Firefox */
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important;
|
||||
/* For Firefox */
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar,
|
||||
|
||||
@@ -37,7 +37,7 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
|
||||
size-[18px]
|
||||
p-[2px]
|
||||
flex items-center justify-center
|
||||
text-sm bg-void-bg-3 text-void-fg-3
|
||||
text-sm text-void-fg-3
|
||||
hover:brightness-110
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}
|
||||
@@ -69,7 +69,7 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1500 // amount of time to say 'Copied!'
|
||||
|
||||
export const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
export const CopyButton = ({ codeStr, toolTipName }: { codeStr: string | (() => Promise<string> | string), toolTipName: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
@@ -83,8 +83,8 @@ export const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}, [copyButtonText])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(codeStr)
|
||||
const onCopy = useCallback(async () => {
|
||||
clipboardService.writeText(typeof codeStr === 'string' ? codeStr : await codeStr())
|
||||
.then(() => { setCopyButtonText(CopyButtonText.Copied) })
|
||||
.catch(() => { setCopyButtonText(CopyButtonText.Error) })
|
||||
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
|
||||
@@ -93,7 +93,7 @@ export const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
return <IconShell1
|
||||
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
|
||||
onClick={onCopy}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Copy' })}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: toolTipName })}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -162,7 +162,6 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
|
||||
)
|
||||
if (shouldUpdate) {
|
||||
rerender(c => c + 1)
|
||||
console.log('rerendering....')
|
||||
}
|
||||
}, [applyBoxId, uri]))
|
||||
|
||||
@@ -322,12 +321,12 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Reject file' })}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Accept file' })}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
@@ -375,7 +374,7 @@ export const BlockCodeApplyWrapper = ({
|
||||
</div>
|
||||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} />}
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} reapplyIcon={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,63 @@ function isValidUri(s: string): boolean {
|
||||
return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like //
|
||||
}
|
||||
|
||||
// renders contiguous string of latex eg $e^{i\pi}$
|
||||
const LatexRender = ({ latex }: { latex: string }) => {
|
||||
return <span className="katex-error text-red-500">{latex}</span>
|
||||
// try {
|
||||
// let formula = latex;
|
||||
// let displayMode = false;
|
||||
|
||||
// // Extract the formula from delimiters
|
||||
// if (latex.startsWith('$') && latex.endsWith('$')) {
|
||||
// // Check if it's display math $$...$$
|
||||
// if (latex.startsWith('$$') && latex.endsWith('$$')) {
|
||||
// formula = latex.slice(2, -2);
|
||||
// displayMode = true;
|
||||
// } else {
|
||||
// formula = latex.slice(1, -1);
|
||||
// }
|
||||
// } else if (latex.startsWith('\\(') && latex.endsWith('\\)')) {
|
||||
// formula = latex.slice(2, -2);
|
||||
// } else if (latex.startsWith('\\[') && latex.endsWith('\\]')) {
|
||||
// formula = latex.slice(2, -2);
|
||||
// displayMode = true;
|
||||
// }
|
||||
|
||||
// // Render LaTeX
|
||||
// const html = katex.renderToString(formula, {
|
||||
// displayMode: displayMode,
|
||||
// throwOnError: false,
|
||||
// output: 'html'
|
||||
// });
|
||||
|
||||
// // Sanitize the HTML output with DOMPurify
|
||||
// const sanitizedHtml = dompurify.sanitize(html, {
|
||||
// RETURN_TRUSTED_TYPE: true,
|
||||
// USE_PROFILES: { html: true, svg: true, mathMl: true }
|
||||
// });
|
||||
|
||||
// // Add proper styling based on mode
|
||||
// const className = displayMode
|
||||
// ? 'katex-block my-2 text-center'
|
||||
// : 'katex-inline';
|
||||
|
||||
// // Use the ref approach to avoid dangerouslySetInnerHTML
|
||||
// const mathRef = React.useRef<HTMLSpanElement>(null);
|
||||
|
||||
// React.useEffect(() => {
|
||||
// if (mathRef.current) {
|
||||
// mathRef.current.innerHTML = sanitizedHtml as unknown as string;
|
||||
// }
|
||||
// }, [sanitizedHtml]);
|
||||
|
||||
// return <span ref={mathRef} className={className}></span>;
|
||||
// } catch (error) {
|
||||
// console.error('KaTeX rendering error:', error);
|
||||
// return <span className="katex-error text-red-500">{latex}</span>;
|
||||
// }
|
||||
}
|
||||
|
||||
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
||||
|
||||
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
|
||||
@@ -57,7 +114,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
|
||||
|
||||
let link = undefined
|
||||
if (rawText.endsWith("`")) { // if codespan was completed
|
||||
if (rawText.endsWith('`')) { // if codespan was completed
|
||||
|
||||
// get link from cache
|
||||
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
|
||||
@@ -108,6 +165,105 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
}
|
||||
|
||||
|
||||
const paragraphToLatexSegments = (paragraphText: string) => {
|
||||
|
||||
const segments: React.ReactNode[] = [];
|
||||
|
||||
if (paragraphText
|
||||
&& !(paragraphText.includes('#') || paragraphText.includes('`')) // don't process latex if a codespan or header tag
|
||||
&& !/^[\w\s.()[\]{}]+$/.test(paragraphText) // don't process latex if string only contains alphanumeric chars, whitespace, periods, and brackets
|
||||
) {
|
||||
const rawText = paragraphText;
|
||||
// Regular expressions to match LaTeX delimiters
|
||||
const displayMathRegex = /\$\$(.*?)\$\$/g; // Display math: $$...$$
|
||||
const inlineMathRegex = /\$((?!\$).*?)\$/g; // Inline math: $...$ (but not $$)
|
||||
|
||||
// Check if the paragraph contains any LaTeX expressions
|
||||
if (displayMathRegex.test(rawText) || inlineMathRegex.test(rawText)) {
|
||||
// Reset the regex state (since we used .test earlier)
|
||||
displayMathRegex.lastIndex = 0;
|
||||
inlineMathRegex.lastIndex = 0;
|
||||
|
||||
// Parse the text into segments of regular text and LaTeX
|
||||
let lastIndex = 0;
|
||||
let segmentId = 0;
|
||||
|
||||
// First replace display math ($$...$$)
|
||||
let match;
|
||||
while ((match = displayMathRegex.exec(rawText)) !== null) {
|
||||
const [fullMatch, formula] = match;
|
||||
const matchIndex = match.index;
|
||||
|
||||
// Add text before the LaTeX expression
|
||||
if (matchIndex > lastIndex) {
|
||||
const textBefore = rawText.substring(lastIndex, matchIndex);
|
||||
segments.push(
|
||||
<span key={`text-${segmentId++}`}>
|
||||
{textBefore}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Add the LaTeX expression
|
||||
segments.push(
|
||||
<LatexRender key={`latex-${segmentId++}`} latex={fullMatch} />
|
||||
);
|
||||
|
||||
lastIndex = matchIndex + fullMatch.length;
|
||||
}
|
||||
|
||||
// Add any remaining text (which might contain inline math)
|
||||
if (lastIndex < rawText.length) {
|
||||
const remainingText = rawText.substring(lastIndex);
|
||||
|
||||
// Process inline math in the remaining text
|
||||
lastIndex = 0;
|
||||
inlineMathRegex.lastIndex = 0;
|
||||
const inlineSegments: React.ReactNode[] = [];
|
||||
|
||||
while ((match = inlineMathRegex.exec(remainingText)) !== null) {
|
||||
const [fullMatch] = match;
|
||||
const matchIndex = match.index;
|
||||
|
||||
// Add text before the inline LaTeX
|
||||
if (matchIndex > lastIndex) {
|
||||
const textBefore = remainingText.substring(lastIndex, matchIndex);
|
||||
inlineSegments.push(
|
||||
<span key={`inline-text-${segmentId++}`}>
|
||||
{textBefore}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Add the inline LaTeX
|
||||
inlineSegments.push(
|
||||
<LatexRender key={`inline-latex-${segmentId++}`} latex={fullMatch} />
|
||||
);
|
||||
|
||||
lastIndex = matchIndex + fullMatch.length;
|
||||
}
|
||||
|
||||
// Add any remaining text after all inline math
|
||||
if (lastIndex < remainingText.length) {
|
||||
inlineSegments.push(
|
||||
<span key={`inline-final-${segmentId++}`}>
|
||||
{remainingText.substring(lastIndex)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
segments.push(...inlineSegments);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
|
||||
export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean }
|
||||
const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): React.ReactNode => {
|
||||
const accessor = useAccessor()
|
||||
@@ -120,11 +276,11 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return null;
|
||||
}
|
||||
|
||||
if (t.type === "space") {
|
||||
if (t.type === 'space') {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
if (t.type === 'code') {
|
||||
const [firstLine, remainingContents] = separateOutFirstLine(t.text)
|
||||
const firstLineIsURI = isValidUri(firstLine) && !codeURI
|
||||
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
|
||||
@@ -152,7 +308,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
}
|
||||
|
||||
if (options.isApplyEnabled && chatMessageLocation) {
|
||||
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with "```")
|
||||
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with '```')
|
||||
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
@@ -179,7 +335,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
if (t.type === 'heading') {
|
||||
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
|
||||
|
||||
@@ -188,25 +344,26 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
if (t.type === 'table') {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index}>
|
||||
{cell.raw}
|
||||
{t.header.map((h, hIdx: number) => (
|
||||
<th key={hIdx}>
|
||||
{h.text}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td key={cellIndex} >
|
||||
{cell.raw}
|
||||
{t.rows.map((row, rowIdx: number) => (
|
||||
<tr key={rowIdx}>
|
||||
{row.map((r, rIdx: number) => (
|
||||
<td key={rIdx} >
|
||||
{r.text}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@@ -217,14 +374,14 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
)
|
||||
// return (
|
||||
// <div>
|
||||
// <table className={"min-w-full border border-void-bg-2"}>
|
||||
// <table className={'min-w-full border border-void-bg-2'}>
|
||||
// <thead>
|
||||
// <tr className="bg-void-bg-1">
|
||||
// <tr className='bg-void-bg-1'>
|
||||
// {t.header.map((cell: any, index: number) => (
|
||||
// <th
|
||||
// key={index}
|
||||
// className="px-4 py-2 border border-void-bg-2 font-semibold"
|
||||
// style={{ textAlign: t.align[index] || "left" }}
|
||||
// className='px-4 py-2 border border-void-bg-2 font-semibold'
|
||||
// style={{ textAlign: t.align[index] || 'left' }}
|
||||
// >
|
||||
// {cell.raw}
|
||||
// </th>
|
||||
@@ -237,8 +394,8 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
// {row.map((cell: any, cellIndex: number) => (
|
||||
// <td
|
||||
// key={cellIndex}
|
||||
// className={"px-4 py-2 border border-void-bg-2"}
|
||||
// style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
// className={'px-4 py-2 border border-void-bg-2'}
|
||||
// style={{ textAlign: t.align[cellIndex] || 'left' }}
|
||||
// >
|
||||
// {cell.raw}
|
||||
// </td>
|
||||
@@ -251,32 +408,32 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
// )
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
if (t.type === 'hr') {
|
||||
return <hr />
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
if (t.type === 'blockquote') {
|
||||
return <blockquote>{t.text}</blockquote>
|
||||
}
|
||||
|
||||
if (t.type === 'list_item') {
|
||||
return <li>
|
||||
<input type="checkbox" checked={t.checked} readOnly />
|
||||
<input type='checkbox' checked={t.checked} readOnly />
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} />
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
const ListTag = t.ordered ? "ol" : "ul"
|
||||
if (t.type === 'list') {
|
||||
const ListTag = t.ordered ? 'ol' : 'ul'
|
||||
|
||||
return (
|
||||
<ListTag start={t.start ? t.start : undefined}>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
<input type='checkbox' checked={item.checked} readOnly />
|
||||
)}
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} inPTag={true} {...options} />
|
||||
@@ -287,7 +444,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
if (t.type === 'paragraph') {
|
||||
|
||||
// check for latex
|
||||
const latexSegments = paragraphToLatexSegments(t.raw)
|
||||
if (latexSegments.length !== 0) {
|
||||
if (inPTag) {
|
||||
return <span className='block'>{latexSegments}</span>;
|
||||
}
|
||||
return <p>{latexSegments}</p>;
|
||||
}
|
||||
|
||||
// if no latex, default behavior
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index}
|
||||
@@ -304,34 +472,28 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return <p>{contents}</p>
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
const contents = t.raw
|
||||
if (inPTag) return <span className='block'>{contents}</span>
|
||||
return <p>{contents}</p>
|
||||
}
|
||||
|
||||
if (t.type === "text" || t.type === "escape") {
|
||||
if (t.type === 'text' || t.type === 'escape' || t.type === 'html') {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "def") {
|
||||
if (t.type === 'def') {
|
||||
return <></> // Definitions are typically not rendered
|
||||
}
|
||||
|
||||
if (t.type === "link") {
|
||||
if (t.type === 'link') {
|
||||
return (
|
||||
<a
|
||||
onClick={() => { window.open(t.href) }}
|
||||
href={t.href}
|
||||
title={t.title ?? undefined}
|
||||
className='underline cursor-pointer hover:brightness-90 transition-all duration-200'
|
||||
className='underline cursor-pointer hover:brightness-90 transition-all duration-200 text-void-fg-2'
|
||||
>
|
||||
{t.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
if (t.type === 'image') {
|
||||
return <img
|
||||
src={t.href}
|
||||
alt={t.text}
|
||||
@@ -340,16 +502,16 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
if (t.type === 'strong') {
|
||||
return <strong>{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
if (t.type === 'em') {
|
||||
return <em>{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
if (t.type === 'codespan') {
|
||||
|
||||
if (options.isLinkDetectionEnabled && chatMessageLocation) {
|
||||
return <CodespanWithLink
|
||||
@@ -363,18 +525,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return <Codespan text={t.text} />
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
if (t.type === 'br') {
|
||||
return <br />
|
||||
}
|
||||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
if (t.type === 'del') {
|
||||
return <del>{t.text}</del>
|
||||
}
|
||||
// default
|
||||
return (
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
|
||||
<span className="text-sm text-orange-500">Unknown token rendered...</span>
|
||||
<div className='bg-orange-50 rounded-sm overflow-hidden p-2'>
|
||||
<span className='text-sm text-orange-500'>Unknown token rendered...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -390,4 +552,3 @@ export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
@@ -51,11 +51,12 @@ class ErrorBoundary extends Component<Props, State> {
|
||||
|
||||
// Use ErrorDisplay component as the default error UI
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={this.state.error + ''}
|
||||
fullError={this.state.error}
|
||||
onDismiss={this.props.onDismiss || null}
|
||||
/>
|
||||
<WarningBox text={this.state.error + ''} />
|
||||
// <ErrorDisplay
|
||||
// message={this.state.error + ''}
|
||||
// fullError={this.state.error}
|
||||
// onDismiss={this.props.onDismiss || null}
|
||||
// />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+368
-86
@@ -3,35 +3,20 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React from "react";
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { CopyButton, IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useFullChatThreadsStreamState, useSettingsState } from '../util/services.js';
|
||||
import { IconX } from './SidebarChat.js';
|
||||
import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserCheck, X } from 'lucide-react';
|
||||
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
export const OldSidebarThreadSelector = () => {
|
||||
|
||||
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useChatThreadsState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
return (
|
||||
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
|
||||
|
||||
@@ -52,72 +37,369 @@ export const SidebarThreadSelector = () => {
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
<div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
|
||||
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
|
||||
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
||||
title={new Date(pastThread.lastModified).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
{/* <OldPastThreadsList /> */}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OldPastThreadsList = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
|
||||
return <div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
|
||||
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false })
|
||||
}}
|
||||
title={new Date(pastThread.lastModified).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const numInitialThreads = 3
|
||||
|
||||
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null)
|
||||
|
||||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
const streamState = useFullChatThreadsStreamState()
|
||||
|
||||
const runningThreadIds: { [threadId: string]: IsRunningType | undefined } = {}
|
||||
for (const threadId in streamState) {
|
||||
const isRunning = streamState[threadId]?.isRunning
|
||||
if (isRunning) { runningThreadIds[threadId] = isRunning }
|
||||
}
|
||||
|
||||
if (!allThreads) {
|
||||
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
|
||||
}
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
// Get only first 5 threads if not showing all
|
||||
const hasMoreThreads = sortedThreadIds.length > numInitialThreads;
|
||||
const displayThreads = showAll ? sortedThreadIds : sortedThreadIds.slice(0, numInitialThreads);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col mb-2 gap-2 w-full text-nowrap text-void-fg-3 select-none relative ${className}`}>
|
||||
{displayThreads.length === 0
|
||||
? <></> // No chats yet... Suggestion: Tell me about my codebase Suggestion: Create a new .voidrules file in the root of my repo
|
||||
: displayThreads.map((threadId, i) => {
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <div key={i} className="p-1">{`Error accessing chat history.`}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<PastThreadElement
|
||||
key={pastThread.id}
|
||||
pastThread={pastThread}
|
||||
idx={i}
|
||||
hoveredIdx={hoveredIdx}
|
||||
setHoveredIdx={setHoveredIdx}
|
||||
isRunning={runningThreadIds[pastThread.id]}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{hasMoreThreads && !showAll && (
|
||||
<div
|
||||
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
onClick={() => setShowAll(true)}
|
||||
>
|
||||
Show {sortedThreadIds.length - numInitialThreads} more...
|
||||
</div>
|
||||
)}
|
||||
{hasMoreThreads && showAll && (
|
||||
<div
|
||||
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
onClick={() => setShowAll(false)}
|
||||
>
|
||||
Show less
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Format date to display as today, yesterday, or date
|
||||
const formatDate = (date: Date) => {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (date >= today) {
|
||||
return 'Today';
|
||||
} else if (date >= yesterday) {
|
||||
return 'Yesterday';
|
||||
} else {
|
||||
return `${date.toLocaleString('default', { month: 'short' })} ${date.getDate()}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Format time to 12-hour format
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const DuplicateButton = ({ threadId }: { threadId: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
return <IconShell1
|
||||
Icon={Copy}
|
||||
className='size-[11px]'
|
||||
onClick={() => { chatThreadsService.duplicateThread(threadId); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Duplicate thread'
|
||||
>
|
||||
</IconShell1>
|
||||
|
||||
}
|
||||
|
||||
const TrashButton = ({ threadId }: { threadId: string }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
|
||||
const [isTrashPressed, setIsTrashPressed] = useState(false)
|
||||
|
||||
return (isTrashPressed ?
|
||||
<div className='flex flex-nowrap text-nowrap gap-1'>
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
className='size-[11px]'
|
||||
onClick={() => { setIsTrashPressed(false); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Cancel'
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
className='size-[11px]'
|
||||
onClick={() => { chatThreadsService.deleteThread(threadId); setIsTrashPressed(false); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Confirm'
|
||||
/>
|
||||
</div>
|
||||
: <IconShell1
|
||||
Icon={Trash2}
|
||||
className='size-[11px]'
|
||||
onClick={() => { setIsTrashPressed(true); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Delete thread'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunning }: {
|
||||
pastThread: ThreadType,
|
||||
idx: number,
|
||||
hoveredIdx: number | null,
|
||||
setHoveredIdx: (idx: number | null) => void,
|
||||
isRunning: IsRunningType | undefined,
|
||||
}
|
||||
|
||||
) => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
// const settingsState = useSettingsState()
|
||||
// const convertService = accessor.get('IConvertToLLMMessageService')
|
||||
// const chatMode = settingsState.globalSettings.chatMode
|
||||
// const modelSelection = settingsState.modelSelectionOfFeature?.Chat ?? null
|
||||
// const copyChatButton = <CopyButton
|
||||
// codeStr={async () => {
|
||||
// const { messages } = await convertService.prepareLLMChatMessages({
|
||||
// chatMessages: currentThread.messages,
|
||||
// chatMode,
|
||||
// modelSelection,
|
||||
// })
|
||||
// return JSON.stringify(messages, null, 2)
|
||||
// }}
|
||||
// toolTipName={modelSelection === null ? 'Copy As Messages Payload' : `Copy As ${displayInfoOfProviderName(modelSelection.providerName).title} Payload`}
|
||||
// />
|
||||
|
||||
|
||||
// const currentThread = chatThreadsService.getCurrentThread()
|
||||
// const copyChatButton2 = <CopyButton
|
||||
// codeStr={async () => {
|
||||
// return JSON.stringify(currentThread.messages, null, 2)
|
||||
// }}
|
||||
// toolTipName={`Copy As Void Chat`}
|
||||
// />
|
||||
|
||||
let firstMsg = null;
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx];
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
const detailsHTML = <span
|
||||
className='gap-1 inline-flex items-center'
|
||||
// data-tooltip-id='void-tooltip'
|
||||
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
|
||||
// data-tooltip-place='top'
|
||||
>
|
||||
{/* <span>{numMessages}</span> */}
|
||||
{formatDate(new Date(pastThread.lastModified))}
|
||||
</span>
|
||||
|
||||
return <div
|
||||
key={pastThread.id}
|
||||
className={`
|
||||
py-1 px-2 rounded text-sm bg-zinc-700/5 hover:bg-zinc-700/10 dark:bg-zinc-300/5 dark:hover:bg-zinc-300/10 cursor-pointer opacity-80 hover:opacity-100
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false });
|
||||
}}
|
||||
onMouseEnter={() => setHoveredIdx(idx)}
|
||||
onMouseLeave={() => setHoveredIdx(null)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
|
||||
{/* spinner */}
|
||||
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
:
|
||||
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
:
|
||||
null}
|
||||
{/* name */}
|
||||
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-x-1 opacity-60">
|
||||
{idx === hoveredIdx ?
|
||||
<>
|
||||
{/* trash icon */}
|
||||
<DuplicateButton threadId={pastThread.id} />
|
||||
|
||||
{/* trash icon */}
|
||||
<TrashButton threadId={pastThread.id} />
|
||||
</>
|
||||
: <>
|
||||
{detailsHTML}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
--void-bg-1: var(--vscode-input-background);
|
||||
--void-bg-1-alt: var(--vscode-badge-background);
|
||||
--void-bg-2: var(--vscode-sideBar-background);
|
||||
--void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
|
||||
--void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%);
|
||||
--void-bg-2-hover: color-mix(in srgb, var(--vscode-editor-foreground) 2%, var(--vscode-sideBar-background) 98%);
|
||||
--void-bg-3: var(--vscode-editor-background);
|
||||
|
||||
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
||||
|
||||
@@ -48,6 +48,80 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
||||
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
|
||||
}
|
||||
|
||||
type GenerateNextOptions = (newPathText: string) => Option[]
|
||||
|
||||
type Option = {
|
||||
name: string,
|
||||
displayName: string,
|
||||
} & (
|
||||
| { nextOptions: Option[], generateNextOptions?: undefined }
|
||||
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions }
|
||||
| { nextOptions?: undefined, generateNextOptions?: undefined }
|
||||
)
|
||||
|
||||
|
||||
const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string[], newPathText: string) => {
|
||||
|
||||
|
||||
const allOptions: Option[] = [
|
||||
{
|
||||
name: 'files',
|
||||
displayName: 'files',
|
||||
generateNextOptions: () => [
|
||||
{ name: 'a.txt', displayName: 'a.txt', },
|
||||
{ name: 'b.txt', displayName: 'b.txt', },
|
||||
{ name: 'c.txt', displayName: 'c.txt', },
|
||||
{ name: 'd.txt', displayName: 'd.txt', },
|
||||
{ name: 'e.txt', displayName: 'e.txt', },
|
||||
{ name: 'f.txt', displayName: 'f.txt', },
|
||||
{ name: 'g.txt', displayName: 'g.txt', },
|
||||
{ name: '!a.txt', displayName: '!a.txt', },
|
||||
{ name: '!b.txt', displayName: '!b.txt', },
|
||||
{ name: '!c.txt', displayName: '!c.txt', },
|
||||
{ name: '!d.txt', displayName: '!d.txt', },
|
||||
{ name: '!e.txt', displayName: '!e.txt', },
|
||||
{ name: '!f.txt', displayName: '!f.txt', },
|
||||
{ name: '!g.txt', displayName: '!g.txt', },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'folders',
|
||||
displayName: 'folders',
|
||||
nextOptions: [
|
||||
{ name: 'FOLDER', displayName: 'FOLDER', },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
// follow the path in the optionsTree (until the last path element)
|
||||
|
||||
let nextOptionsAtPath = allOptions
|
||||
let generateNextOptionsAtPath: GenerateNextOptions | undefined = undefined
|
||||
|
||||
for (const pn of path) {
|
||||
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.name.toLowerCase() === pn.toLowerCase())
|
||||
|
||||
if (!selectedOption) return;
|
||||
|
||||
nextOptionsAtPath = selectedOption.nextOptions! // assume nextOptions exists until we hit the very last option (the path will never contain the last possible option)
|
||||
generateNextOptionsAtPath = selectedOption.generateNextOptions
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (generateNextOptionsAtPath) {
|
||||
nextOptionsAtPath = generateNextOptionsAtPath(newPathText)
|
||||
}
|
||||
|
||||
const optionsAtPath = nextOptionsAtPath.filter(o => o.name.includes(newPathText))
|
||||
|
||||
|
||||
return optionsAtPath
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
|
||||
type InputBox2Props = {
|
||||
@@ -64,8 +138,235 @@ type InputBox2Props = {
|
||||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
|
||||
|
||||
// mirrors whatever is in ref
|
||||
const accessor = useAccessor()
|
||||
const toolsService = accessor.get('IToolsService')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const selectedOptionRef = useRef<HTMLDivElement>(null);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const [path, setPath] = useState<string[]>([]);
|
||||
const [optionIdx, setOptionIdx] = useState<number>(0);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [newPathText, setNewPathText] = useState<string>('');
|
||||
|
||||
|
||||
const insertTextAtCursor = (text: string) => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
// Focus the textarea first
|
||||
textarea.focus();
|
||||
|
||||
// The most reliable way to simulate typing is to use execCommand
|
||||
// which will trigger all the appropriate native events
|
||||
document.execCommand('insertText', false, text);
|
||||
|
||||
// React's onChange relies on a SyntheticEvent system
|
||||
// The best way to ensure it runs is to call callbacks directly
|
||||
if (onChangeText) {
|
||||
onChangeText(textarea.value);
|
||||
}
|
||||
adjustHeight();
|
||||
};
|
||||
|
||||
|
||||
|
||||
const onSelectOption = () => {
|
||||
|
||||
if (!options.length) { return; }
|
||||
|
||||
const option = options[optionIdx];
|
||||
const newPath = [...path, option.name]
|
||||
const isLastOption = !option.generateNextOptions && !option.nextOptions
|
||||
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
setOptionIdx(0)
|
||||
if (isLastOption) {
|
||||
setIsMenuOpen(false)
|
||||
insertTextAtCursor(`TODO-${option.displayName}`)
|
||||
}
|
||||
else {
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveOption = () => {
|
||||
const newPath = [...path.slice(0, path.length - 1)]
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
setOptionIdx(0)
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
}
|
||||
|
||||
const onOpenOptionMenu = () => {
|
||||
setPath([])
|
||||
setNewPathText('')
|
||||
setIsMenuOpen(true);
|
||||
setOptionIdx(0);
|
||||
setOptions(getOptionsAtPath(accessor, [], '') || []);
|
||||
}
|
||||
const onCloseOptionMenu = () => {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
|
||||
const onNavigateUp = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx - 1 + options.length) % options.length);
|
||||
}
|
||||
const onNavigateDown = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx + 1) % options.length);
|
||||
}
|
||||
|
||||
const onPathTextChange = (newStr: string) => {
|
||||
setNewPathText(newStr);
|
||||
setOptions(getOptionsAtPath(accessor, path, newStr) || []);
|
||||
|
||||
}
|
||||
|
||||
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'ArrowUp') {
|
||||
onNavigateUp();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
onNavigateDown();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'Enter') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'Escape') {
|
||||
onCloseOptionMenu()
|
||||
} else if (e.key === 'Backspace') {
|
||||
|
||||
if (!newPathText) { // No text remaining
|
||||
if (path.length === 0) {
|
||||
onCloseOptionMenu()
|
||||
} else {
|
||||
onRemoveOption();
|
||||
}
|
||||
}
|
||||
else if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+Backspace
|
||||
onPathTextChange('')
|
||||
}
|
||||
else { // Backspace
|
||||
onPathTextChange(newPathText.slice(0, -1))
|
||||
}
|
||||
} else if (e.key.length === 1) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+letter
|
||||
// do nothing
|
||||
}
|
||||
else { // letter
|
||||
onPathTextChange(newPathText + e.key)
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
};
|
||||
|
||||
// scroll the selected optionIdx into view on optionIdx and newPathText changes
|
||||
useEffect(() => {
|
||||
if (isMenuOpen && selectedOptionRef.current) {
|
||||
selectedOptionRef.current.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]);
|
||||
|
||||
|
||||
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
const gapPx = 2
|
||||
const offsetPx = 2
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
strategy,
|
||||
refs,
|
||||
middlewareData,
|
||||
update
|
||||
} = useFloating({
|
||||
open: isMenuOpen,
|
||||
onOpenChange: setIsMenuOpen,
|
||||
placement: 'top',
|
||||
|
||||
middleware: [
|
||||
offset({ mainAxis: gapPx, crossAxis: offsetPx }),
|
||||
flip({
|
||||
boundary: document.body,
|
||||
padding: 8
|
||||
}),
|
||||
shift({
|
||||
boundary: document.body,
|
||||
padding: 8,
|
||||
}),
|
||||
size({
|
||||
apply({ availableHeight, elements, rects }) {
|
||||
const maxHeight = Math.min(availableHeight)
|
||||
|
||||
Object.assign(elements.floating.style, {
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY: 'auto',
|
||||
// Ensure the width isn't constrained by the parent
|
||||
width: `${Math.max(
|
||||
rects.reference.width,
|
||||
measureRef.current?.offsetWidth ?? 0
|
||||
)}px`
|
||||
});
|
||||
},
|
||||
padding: 8,
|
||||
// Use viewport as boundary instead of any parent element
|
||||
boundary: document.body,
|
||||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
strategy: 'fixed',
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
const floating = refs.floating.current;
|
||||
const reference = refs.reference.current;
|
||||
|
||||
// Check if reference is an HTML element before using contains
|
||||
const isReferenceHTMLElement = reference && 'contains' in reference;
|
||||
|
||||
if (
|
||||
floating &&
|
||||
(!isReferenceHTMLElement || !reference.contains(target)) &&
|
||||
!floating.contains(target)
|
||||
) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isMenuOpen, refs.floating, refs.reference]);
|
||||
|
||||
|
||||
|
||||
const [isEnabled, setEnabled] = useState(true)
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
@@ -104,18 +405,20 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
|
||||
|
||||
|
||||
return (
|
||||
return <>
|
||||
<textarea
|
||||
autoFocus={false}
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
if (fnsRef)
|
||||
fnsRef.current = fns
|
||||
|
||||
refs.setReference(r)
|
||||
|
||||
textAreaRef.current = r
|
||||
if (typeof ref === 'function') ref(r)
|
||||
else if (ref) ref.current = r
|
||||
adjustHeight()
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref, refs])}
|
||||
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
@@ -130,7 +433,16 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
// inputBorder: asCssVariable(inputBorder),
|
||||
}}
|
||||
|
||||
onChange={useCallback(() => {
|
||||
onInput={useCallback((event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const latestChange = (event.nativeEvent as InputEvent).data;
|
||||
|
||||
if (latestChange === '@') {
|
||||
onOpenOptionMenu()
|
||||
}
|
||||
|
||||
}, [onOpenOptionMenu, accessor])}
|
||||
|
||||
onChange={useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
onChangeText?.(r.value)
|
||||
@@ -138,18 +450,75 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
}, [onChangeText, adjustHeight])}
|
||||
|
||||
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
if (isMenuOpen) {
|
||||
onMenuKeyDown(e)
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
// Shift + Enter when multiline = newline
|
||||
const shouldAddNewline = e.shiftKey && multiline
|
||||
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
|
||||
}
|
||||
onKeyDown?.(e)
|
||||
}, [onKeyDown, multiline])}
|
||||
}, [onKeyDown, onMenuKeyDown, multiline])}
|
||||
|
||||
rows={1}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)
|
||||
{/* <div>{`idx ${optionIdx}`}</div> */}
|
||||
{isMenuOpen && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0
|
||||
}}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="py-1">
|
||||
{/* Path navigation breadcrumbs */}
|
||||
<div className="px-2 py-1 text-void-fg-3 text-sm border-b border-void-border-3">
|
||||
{[...path, newPathText].join(' > ')}
|
||||
</div>
|
||||
|
||||
{/* Options list */}
|
||||
{options.length === 0 ? (
|
||||
<div className="px-3 py-2 text-void-fg-3">No options available</div>
|
||||
) : (
|
||||
options.map((o, oIdx) => (
|
||||
<div
|
||||
ref={oIdx === optionIdx ? selectedOptionRef : null}
|
||||
|
||||
key={o.name}
|
||||
className={`px-3 py-1.5 cursor-pointer bg-void-bg-2 ${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}`}
|
||||
onClick={() => { onSelectOption(); }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-void-fg-1">{o.displayName}</span>
|
||||
{o.nextOptions || o.generateNextOptions ? (
|
||||
<svg className="ml-2 h-3 w-3 text-void-fg-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M4.5 2.5L8 6L4.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
})
|
||||
|
||||
@@ -664,57 +1033,60 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg"
|
||||
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: matchInputWidth
|
||||
width: (matchInputWidth
|
||||
? (refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0)
|
||||
: Math.max(
|
||||
(refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0),
|
||||
(measureRef.current instanceof HTMLElement ? measureRef.current.offsetWidth : 0)
|
||||
),
|
||||
))
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
><div className='overflow-auto max-h-80'>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
|
||||
transition-all duration-100
|
||||
bg-void-bg-1
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
|
||||
`}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex justify-between items-center w-full gap-x-1">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex justify-between w-full">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -48,6 +48,7 @@ import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
|
||||
import { INativeHostService } from '../../../../../../../platform/native/common/native.js';
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
import { IToolsService } from '../../../toolsService.js'
|
||||
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
@@ -217,6 +218,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||
INativeHostService: accessor.get(INativeHostService),
|
||||
IToolsService: accessor.get(IToolsService),
|
||||
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
@@ -302,6 +304,16 @@ export const useChatThreadsStreamState = (threadId: string) => {
|
||||
return s
|
||||
}
|
||||
|
||||
export const useFullChatThreadsStreamState = () => {
|
||||
const [s, ss] = useState(chatThreadsStreamState)
|
||||
useEffect(() => {
|
||||
ss(chatThreadsStreamState)
|
||||
const listener = () => { ss(chatThreadsStreamState) }
|
||||
chatThreadsStreamStateListeners.add(listener)
|
||||
return () => { chatThreadsStreamStateListeners.delete(listener) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -111,7 +111,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const { model } = await voidModelService.getModelSafe(nextURI)
|
||||
if (model) {
|
||||
// switch to the URI
|
||||
editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor)
|
||||
editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,12 +245,12 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
// </button>
|
||||
|
||||
const acceptAllButton = <AcceptAllButtonWrapper
|
||||
text={'Accept File'}
|
||||
text={'Keep Changes'}
|
||||
onClick={onAcceptAll}
|
||||
/>
|
||||
|
||||
const rejectAllButton = <RejectAllButtonWrapper
|
||||
text={'Reject File'}
|
||||
text={'Reject All'}
|
||||
onClick={onRejectAll}
|
||||
/>
|
||||
|
||||
|
||||
+406
-248
@@ -3,19 +3,22 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
|
||||
import { Check, ExternalLink, X } from 'lucide-react';
|
||||
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
|
||||
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
|
||||
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
|
||||
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
|
||||
|
||||
const OVERRIDE_VALUE = false
|
||||
|
||||
export const VoidOnboarding = () => {
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete
|
||||
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete || OVERRIDE_VALUE
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
@@ -27,20 +30,47 @@ export const VoidOnboarding = () => {
|
||||
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
|
||||
`}
|
||||
>
|
||||
<VoidOnboardingContent />
|
||||
<ErrorBoundary>
|
||||
<VoidOnboardingContent />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const VoidIcon = () => {
|
||||
const accessor = useAccessor()
|
||||
const themeService = accessor.get('IThemeService')
|
||||
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// void icon style
|
||||
const updateTheme = () => {
|
||||
const theme = themeService.getColorTheme().type
|
||||
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
|
||||
if (divRef.current) {
|
||||
divRef.current.style.maxWidth = '220px'
|
||||
divRef.current.style.opacity = '50%'
|
||||
divRef.current.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
|
||||
}
|
||||
}
|
||||
updateTheme()
|
||||
const d = themeService.onDidColorThemeChange(updateTheme)
|
||||
return () => d.dispose()
|
||||
}, [])
|
||||
|
||||
return <div ref={divRef} className='@@void-void-icon' />
|
||||
}
|
||||
|
||||
const FADE_DURATION_MS = 2000
|
||||
|
||||
|
||||
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { children: React.ReactNode, delayMs?: number, durationMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
|
||||
const [opacity, setOpacity] = useState(0)
|
||||
|
||||
const effectiveDurationMs = durationMs ?? FADE_DURATION_MS
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -52,7 +82,7 @@ const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: Reac
|
||||
|
||||
|
||||
return (
|
||||
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
|
||||
<div className={className} style={{ opacity, transition: `opacity ${effectiveDurationMs}ms ease-in-out` }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -100,15 +130,14 @@ const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: Reac
|
||||
// content
|
||||
// prev/next
|
||||
|
||||
|
||||
const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 bg-[#0e70c0] enabled:hover:bg-[#1177cb] disabled:opacity-50 disabled:cursor-not-allowed rounded text-white duration-300 transition-all"
|
||||
className="px-6 py-2 bg-zinc-100 enabled:hover:bg-zinc-100 disabled:bg-zinc-100/40 disabled:cursor-not-allowed rounded text-black duration-600 transition-all"
|
||||
{...props.disabled && {
|
||||
'data-tooltip-id': 'void-tooltip',
|
||||
'data-tooltip-content': 'Disabled (Please enter all required fields or choose another Provider)',
|
||||
'data-tooltip-content': 'Please enter all required fields or choose another provider',
|
||||
'data-tooltip-place': 'top',
|
||||
}}
|
||||
{...props}
|
||||
@@ -118,23 +147,11 @@ const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.Butto
|
||||
)
|
||||
}
|
||||
|
||||
const SkipButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3 text-void-fg-2 duration-300 transition-all"
|
||||
{...props}
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 rounded text-void-fg-3 opacity-80 hover:brightness-110 duration-300 transition-all"
|
||||
className="px-6 py-2 rounded text-void-fg-3 opacity-80 hover:brightness-115 duration-600 transition-all"
|
||||
{...props}
|
||||
>
|
||||
Back
|
||||
@@ -152,10 +169,10 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
|
||||
className?: string,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`min-h-full flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
|
||||
<FadeIn className='w-full pt-16'>{top}</FadeIn>
|
||||
<FadeIn className='w-full my-auto'>{content}</FadeIn>
|
||||
<div className='w-full pb-8'>{bottom}</div>
|
||||
<div className={`min-h-full text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
|
||||
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
|
||||
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
|
||||
{bottom && <div className='w-full pb-8'>{bottom}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -168,7 +185,7 @@ const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb
|
||||
href={`https://ollama.com/library/${modelName}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
|
||||
className="flex items-center justify-center text-void-fg-2 hover:text-void-fg-1"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
@@ -250,9 +267,9 @@ const YesNoText = ({ val }: { val: boolean | null }) => {
|
||||
|
||||
return <div
|
||||
className={
|
||||
val === true ? "text text-green-500"
|
||||
: val === false ? 'text-red-500'
|
||||
: "text text-yellow-500"
|
||||
val === true ? "text text-emerald-500"
|
||||
: val === false ? 'text-rose-600'
|
||||
: "text text-amber-300"
|
||||
}
|
||||
>
|
||||
{
|
||||
@@ -289,11 +306,11 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||
|
||||
|
||||
// info used to show the table
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
|
||||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.isDefault,
|
||||
showAsDefault: m.type !== 'custom',
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
@@ -303,7 +320,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||
for (const modelName of ollamaRecommendedModels) {
|
||||
if (modelName in infoOfModelName) continue
|
||||
infoOfModelName[modelName] = {
|
||||
...infoOfModelName[modelName],
|
||||
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
|
||||
showAsDefault: true,
|
||||
}
|
||||
}
|
||||
@@ -325,7 +342,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(infoOfModelName).map(modelName => {
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
|
||||
|
||||
|
||||
const capabilities = getModelCapabilities(providerName, modelName)
|
||||
@@ -342,7 +359,6 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||
} = capabilities
|
||||
|
||||
// TODO update this when tools work
|
||||
const supportsTools = !!!((capabilities as unknown as any).supportsTools)
|
||||
|
||||
const removeModelButton = <button
|
||||
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
|
||||
@@ -354,7 +370,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||
|
||||
|
||||
return (
|
||||
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 relative">
|
||||
{!showAsDefault && removeModelButton}
|
||||
{modelName}
|
||||
@@ -362,12 +378,12 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||
<td className="py-2 px-3">${cost.output ?? ''}</td>
|
||||
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
|
||||
<td className="py-2 px-3"><YesNoText val={true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!supportsTools} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
|
||||
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
|
||||
{isDetectableLocally && <td className="py-2 px-3">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
|
||||
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
|
||||
{providerName === 'ollama' && <th className="py-2 px-3">
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
</th>}
|
||||
|
||||
</tr>
|
||||
@@ -375,10 +391,13 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||
})}
|
||||
<tr className="hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 text-void-accent">
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true} />
|
||||
<ErrorBoundary>
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</td>
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
@@ -388,6 +407,57 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||
|
||||
|
||||
|
||||
const PrimaryActionButton = ({ children, className, ringSize, ...props }: { children: React.ReactNode, ringSize?: undefined | 'xl' | 'screen' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
flex items-center justify-center
|
||||
|
||||
text-white dark:text-black
|
||||
bg-black/90 dark:bg-white/90
|
||||
|
||||
${ringSize === 'xl' ? `
|
||||
gap-2 px-16 py-8
|
||||
hover:ring-8 active:ring-8
|
||||
transition-all duration-300 ease-in-out
|
||||
`
|
||||
: ringSize === 'screen' ? `
|
||||
gap-2 px-16 py-8
|
||||
ring-[3000px]
|
||||
transition-all duration-1000 ease-in-out
|
||||
`: ringSize === undefined ? `
|
||||
gap-1 px-4 py-2
|
||||
hover:ring-2 active:ring-2
|
||||
transition-all duration-300 ease-in-out
|
||||
`: ''}
|
||||
|
||||
hover:ring-black/90 dark:hover:ring-white/90
|
||||
active:ring-black/90 dark:active:ring-white/90
|
||||
|
||||
rounded-lg
|
||||
group
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight
|
||||
className={`
|
||||
transition-all duration-300 ease-in-out
|
||||
|
||||
transform
|
||||
group-hover:translate-x-1
|
||||
group-active:translate-x-1
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
|
||||
|
||||
const VoidOnboardingContent = () => {
|
||||
@@ -404,18 +474,42 @@ const VoidOnboardingContent = () => {
|
||||
// page 1 state
|
||||
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
|
||||
|
||||
// page 2 state
|
||||
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(null)
|
||||
// Replace the single selectedProviderName with four separate states
|
||||
// page 2 state - each tab gets its own state
|
||||
const [selectedIntelligentProvider, setSelectedIntelligentProvider] = useState<ProviderName>('anthropic');
|
||||
const [selectedPrivateProvider, setSelectedPrivateProvider] = useState<ProviderName>('ollama');
|
||||
const [selectedAffordableProvider, setSelectedAffordableProvider] = useState<ProviderName>('gemini');
|
||||
const [selectedAllProvider, setSelectedAllProvider] = useState<ProviderName>('anthropic');
|
||||
|
||||
// Helper function to get the current selected provider based on active tab
|
||||
const getSelectedProvider = (): ProviderName => {
|
||||
switch (wantToUseOption) {
|
||||
case 'smart': return selectedIntelligentProvider;
|
||||
case 'private': return selectedPrivateProvider;
|
||||
case 'cheap': return selectedAffordableProvider;
|
||||
case 'all': return selectedAllProvider;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to set the selected provider for the current tab
|
||||
const setSelectedProvider = (provider: ProviderName) => {
|
||||
switch (wantToUseOption) {
|
||||
case 'smart': setSelectedIntelligentProvider(provider); break;
|
||||
case 'private': setSelectedPrivateProvider(provider); break;
|
||||
case 'cheap': setSelectedAffordableProvider(provider); break;
|
||||
case 'all': setSelectedAllProvider(provider); break;
|
||||
}
|
||||
}
|
||||
|
||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
all: providerNames,
|
||||
// TODO allow user to redo onboarding
|
||||
}
|
||||
|
||||
|
||||
const selectedProviderName = getSelectedProvider();
|
||||
const didFillInProviderSettings = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName]._didFillInProviderSettings
|
||||
const isApiKeyLongEnoughIfApiKeyExists = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].apiKey ? voidSettingsState.settingsOfProvider[selectedProviderName].apiKey.length > 15 : true
|
||||
const isAtLeastOneModel = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].models.length >= 1
|
||||
@@ -423,7 +517,7 @@ const VoidOnboardingContent = () => {
|
||||
const didFillInSelectedProviderSettings = !!(didFillInProviderSettings && isApiKeyLongEnoughIfApiKeyExists && isAtLeastOneModel)
|
||||
|
||||
const prevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
@@ -435,11 +529,24 @@ const VoidOnboardingContent = () => {
|
||||
</div>
|
||||
|
||||
|
||||
const lastPagePrevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
<PrimaryActionButton
|
||||
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
|
||||
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
>Enter the Void</PrimaryActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
// cannot be md
|
||||
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
|
||||
smart: "Models with the best performance on benchmarks.",
|
||||
private: "Fully private and hosted on your computer/network.",
|
||||
cheap: "Free and low-cost options.",
|
||||
private: "Host on your computer or local network for full data privacy.",
|
||||
cheap: "Free and affordable options.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
@@ -447,24 +554,25 @@ const VoidOnboardingContent = () => {
|
||||
const detailedDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
|
||||
smart: "Most intelligent and best for agent mode.",
|
||||
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:founders@voideditor.com) for help setting up at your company.",
|
||||
cheap: "Great deals like Gemini 2.5 Pro or self-host a model with Ollama or vLLM for free.",
|
||||
cheap: "Use great deals like Gemini 2.5 Pro, or self-host a model with Ollama or vLLM for free.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// set the selected provider name appropriately
|
||||
// Modified: initialize separate provider states on initial render instead of watching wantToUseOption changes
|
||||
useEffect(() => {
|
||||
if (wantToUseOption && providerNamesOfWantToUseOption[wantToUseOption].length > 0) {
|
||||
setSelectedProviderName(providerNamesOfWantToUseOption[wantToUseOption][0]);
|
||||
} else {
|
||||
setSelectedProviderName(null);
|
||||
if (selectedIntelligentProvider === undefined) {
|
||||
setSelectedIntelligentProvider(providerNamesOfWantToUseOption['smart'][0]);
|
||||
}
|
||||
}, [wantToUseOption]);
|
||||
|
||||
// set wantToUseOption to smart when page changes
|
||||
useEffect(() => {
|
||||
setWantToUseOption(wantToUseOption);
|
||||
}, [pageIndex]);
|
||||
|
||||
if (selectedPrivateProvider === undefined) {
|
||||
setSelectedPrivateProvider(providerNamesOfWantToUseOption['private'][0]);
|
||||
}
|
||||
if (selectedAffordableProvider === undefined) {
|
||||
setSelectedAffordableProvider(providerNamesOfWantToUseOption['cheap'][0]);
|
||||
}
|
||||
if (selectedAllProvider === undefined) {
|
||||
setSelectedAllProvider(providerNamesOfWantToUseOption['all'][0]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// reset the page to page 0 if the user redos onboarding
|
||||
useEffect(() => {
|
||||
@@ -474,213 +582,261 @@ const VoidOnboardingContent = () => {
|
||||
}, [setPageIndex, voidSettingsState.globalSettings.isOnboardingComplete])
|
||||
|
||||
|
||||
// TODO add a description next to the skip button saying (you can always restart the onboarding in Settings)
|
||||
const contentOfIdx: { [pageIndex: number]: React.ReactNode } = {
|
||||
// 0: <OnboardingPageShell
|
||||
// top={
|
||||
// <div className='bg-green-600 h-6 w-32' />
|
||||
// }
|
||||
// content={
|
||||
// <div className='bg-red-600 h-[10000px] w-32' />
|
||||
// }
|
||||
// bottom={
|
||||
// <div className='bg-blue-600 h-6 w-32' />
|
||||
// }
|
||||
// />,
|
||||
0: <OnboardingPageShell
|
||||
top={
|
||||
<div className="text-5xl font-light text-center">Welcome to Void</div>
|
||||
}
|
||||
content={
|
||||
<FadeIn
|
||||
delayMs={500}
|
||||
className="text-center"
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
>
|
||||
Get Started
|
||||
</FadeIn>
|
||||
}
|
||||
bottom={
|
||||
''
|
||||
<div className='flex flex-col items-center gap-8'>
|
||||
<div className="text-5xl font-light text-center">Welcome to Void</div>
|
||||
|
||||
{/* Slice of Void image */}
|
||||
<div className='max-w-md w-full h-[30vh] mx-auto flex items-center justify-center'>
|
||||
<VoidIcon />
|
||||
</div>
|
||||
|
||||
|
||||
<FadeIn
|
||||
delayMs={1000}
|
||||
>
|
||||
<PrimaryActionButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
>
|
||||
Get Started
|
||||
</PrimaryActionButton>
|
||||
</FadeIn>
|
||||
|
||||
</div>
|
||||
}
|
||||
/>,
|
||||
1: <OnboardingPageShell
|
||||
|
||||
hasMaxWidth={false}
|
||||
top={
|
||||
<FadeIn className='flex flex-col items-center'>
|
||||
<div className="text-5xl font-light text-center">AI Preferences</div>
|
||||
top={<></>}
|
||||
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
|
||||
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
|
||||
|
||||
<div className="mt-[10%] text-base text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
|
||||
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
|
||||
|
||||
<div className="flex justify-center w-full md:flex-nowrap md:max-w-[80%] max-w-[90%] gap-4">
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
|
||||
<span className="text-5xl mb-4 relative z-10">🧠</span>
|
||||
<h3 className="text-xl font-medium mb-3 relative z-10">Intelligence</h3>
|
||||
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['smart']}</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Brain size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
|
||||
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
|
||||
<span className="text-5xl mb-4 relative z-10">🔒</span>
|
||||
<h3 className="text-xl font-medium mb-3 relative z-10">Privacy</h3>
|
||||
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['private']}</p>
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Lock size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Private</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
|
||||
<span className="text-5xl mb-4 relative z-10">💵</span>
|
||||
<h3 className="text-xl font-medium mb-3 relative z-10">Affordability</h3>
|
||||
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['cheap']}</p>
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<DollarSign size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</FadeIn>
|
||||
|
||||
</div>}
|
||||
bottom={
|
||||
<div className='mx-auto w-full max-w-[800px]'>
|
||||
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
|
||||
</div>
|
||||
}
|
||||
content={<></>}
|
||||
/>,
|
||||
2: <OnboardingPageShell
|
||||
top={
|
||||
<div className='flex flex-col items-center'>
|
||||
<>
|
||||
{/* Title */}
|
||||
<div className="text-5xl font-light text-center">Choose a Provider</div>
|
||||
|
||||
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
|
||||
|
||||
{/* Preference Selector */}
|
||||
<div className="mt-6 mb-6 mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md">
|
||||
<button
|
||||
onClick={() => {
|
||||
setWantToUseOption('smart');
|
||||
}}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
|
||||
${wantToUseOption === 'smart'
|
||||
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Intelligent
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setWantToUseOption('private');
|
||||
}}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
|
||||
${wantToUseOption === 'private'
|
||||
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Private
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setWantToUseOption('cheap');
|
||||
}}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
|
||||
${wantToUseOption === 'cheap'
|
||||
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Low-Cost
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setWantToUseOption('all')
|
||||
}}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
|
||||
${wantToUseOption === 'all'
|
||||
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}
|
||||
`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
|
||||
>
|
||||
{[
|
||||
{ id: 'smart', label: 'Intelligent' },
|
||||
{ id: 'private', label: 'Private' },
|
||||
{ id: 'cheap', label: 'Affordable' },
|
||||
{ id: 'all', label: 'All' }
|
||||
].map(option => (
|
||||
<ErrorBoundary
|
||||
key={option.id}
|
||||
>
|
||||
<button
|
||||
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
|
||||
? 'dark:text-white text-black font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={`${option.label} providers`}
|
||||
data-tooltip-place='bottom'
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Provider Buttons */}
|
||||
<div
|
||||
key={wantToUseOption}
|
||||
className="mb-2 flex flex-wrap items-center w-full"
|
||||
>
|
||||
|
||||
{(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => {
|
||||
const isSelected = selectedProviderName === providerName
|
||||
{/* Provider Buttons - Modified to use separate components for each tab */}
|
||||
<div className="mb-2 w-full">
|
||||
{/* Intelligent tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
|
||||
const isSelected = selectedIntelligentProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedIntelligentProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedProviderName(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-colors duration-150 border
|
||||
${isSelected ? 'bg-[#0e70c0] text-white shadow-sm border-[#0e70c0]/80' : 'bg-[#0e70c0]/10 text-void-fg-3 hover:bg-[#0e70c0]/30 border-[#0e70c0]/20'}
|
||||
`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Private tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['private'].map((providerName) => {
|
||||
const isSelected = selectedPrivateProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedPrivateProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Affordable tab */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
|
||||
const isSelected = selectedAffordableProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAffordableProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* All tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
|
||||
{providerNames.map((providerName) => {
|
||||
const isSelected = selectedAllProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAllProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
|
||||
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
|
||||
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* ModelsTable and ProviderFields */}
|
||||
{selectedProviderName && <div className='mt-4'>
|
||||
|
||||
|
||||
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
|
||||
{/* Models Table */}
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
<ErrorBoundary>
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Add provider section - simplified styling */}
|
||||
<div className='mb-5 mt-8'>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
<div className='mb-5 mt-8 mx-auto'>
|
||||
<ErrorBoundary>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
|
||||
<div className='my-4'>
|
||||
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
<ErrorBoundary>
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Button and status indicators */}
|
||||
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
|
||||
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
|
||||
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
|
||||
: <div className="mt-2"><AnimatedCheckmarkButton text='Added' /></div>}
|
||||
<ErrorBoundary>
|
||||
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
|
||||
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
|
||||
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
|
||||
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
bottom={
|
||||
prevAndNextButtons
|
||||
<ErrorBoundary>
|
||||
<FadeIn delayMs={50} durationMs={10}>
|
||||
{prevAndNextButtons}
|
||||
</FadeIn>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
/>,
|
||||
@@ -699,7 +855,8 @@ const VoidOnboardingContent = () => {
|
||||
// {prevAndNextButtons}
|
||||
// </div>,
|
||||
3: <OnboardingPageShell
|
||||
top={
|
||||
|
||||
content={
|
||||
<div>
|
||||
<div className="text-5xl font-light text-center">Settings and Themes</div>
|
||||
|
||||
@@ -711,35 +868,36 @@ const VoidOnboardingContent = () => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
bottom={prevAndNextButtons}
|
||||
/>,
|
||||
4: <OnboardingPageShell
|
||||
top={
|
||||
<div className="text-5xl font-light text-center">Jump in</div>
|
||||
}
|
||||
content={
|
||||
<div
|
||||
className="text-center"
|
||||
onClick={() => {
|
||||
// TODO make a fadeout effect
|
||||
voidSettingsService.setGlobalSetting('isOnboardingComplete', true)
|
||||
}}
|
||||
|
||||
>
|
||||
Enter the Void
|
||||
</div>
|
||||
}
|
||||
bottom={
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
}
|
||||
bottom={lastPagePrevAndNextButtons}
|
||||
// bottom={prevAndNextButtons}
|
||||
/>,
|
||||
// 4: <OnboardingPageShell
|
||||
// content={
|
||||
// <>
|
||||
// <div
|
||||
// className='flex justify-center'
|
||||
// >
|
||||
// <PrimaryActionButton
|
||||
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
|
||||
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
// className='text-4xl'
|
||||
// >Enter the Void</PrimaryActionButton>
|
||||
// </div>
|
||||
// </>
|
||||
// }
|
||||
// bottom={
|
||||
// <PreviousButton
|
||||
// onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
// />
|
||||
// }
|
||||
// />,
|
||||
}
|
||||
|
||||
|
||||
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-auto flex flex-col items-center justify-around">
|
||||
{contentOfIdx[pageIndex]}
|
||||
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
|
||||
<ErrorBoundary>
|
||||
{contentOfIdx[pageIndex]}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
+4
-32
@@ -12,6 +12,7 @@ import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { modelFilterOfFeatureName, ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
|
||||
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
|
||||
if (m1.length !== m2.length) return false
|
||||
@@ -44,37 +45,6 @@ const ModelSelectBox = ({ options, featureName, className }: { options: ModelOpt
|
||||
matchInputWidth={false}
|
||||
/>
|
||||
}
|
||||
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
// const accessor = useAccessor()
|
||||
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// let weChangedText = false
|
||||
|
||||
// return <VoidSelectBox
|
||||
// className='@@[&_select]:!void-text-xs text-void-fg-3'
|
||||
// options={options}
|
||||
// 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 MemoizedModelDropdown = ({ featureName, className }: { featureName: FeatureName, className: string }) => {
|
||||
@@ -123,5 +93,7 @@ export const ModelDropdown = ({ featureName, className }: { featureName: Feature
|
||||
: 'Provider required'
|
||||
} />
|
||||
|
||||
return <MemoizedModelDropdown featureName={featureName} className={className} />
|
||||
return <ErrorBoundary>
|
||||
<MemoizedModelDropdown featureName={featureName} className={className} />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js'
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
@@ -16,7 +16,8 @@ import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
import { os } from '../../../../common/helpers/systemInfo.js'
|
||||
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
|
||||
|
||||
import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'
|
||||
import Severity from '../../../../../../../base/common/severity.js'
|
||||
|
||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||
|
||||
@@ -121,7 +122,7 @@ export const AnimatedCheckmarkButton = ({ text, className }: { text?: string, cl
|
||||
|
||||
return <div
|
||||
className={`flex items-center gap-1.5 w-fit
|
||||
${className ? className : `px-2 py-0.5 text-xs text-white bg-[#0e70c0] rounded-sm`}
|
||||
${className ? className : `px-2 py-0.5 text-xs text-zinc-900 bg-zinc-100 rounded-sm`}
|
||||
`}
|
||||
>
|
||||
<svg className="size-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -146,12 +147,42 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t
|
||||
|
||||
return <button
|
||||
disabled={disabled}
|
||||
className={`bg-[#0e70c0] px-3 py-1 text-white rounded-sm ${!disabled ? 'hover:bg-[#1177cb] cursor-pointer' : 'opacity-50 cursor-not-allowed bg-opacity-70'}`}
|
||||
className={`bg-[#0e70c0] px-3 py-1 text-white dark:text-black rounded-sm ${!disabled ? 'hover:bg-[#1177cb] cursor-pointer' : 'opacity-50 cursor-not-allowed bg-opacity-70'}`}
|
||||
{...props}
|
||||
>{text}</button>
|
||||
|
||||
}
|
||||
|
||||
// ConfirmButton prompts for a second click to confirm an action, cancels if clicking outside
|
||||
const ConfirmButton = ({ children, onConfirm, className }: { children: React.ReactNode, onConfirm: () => void, className?: string }) => {
|
||||
const [confirm, setConfirm] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (!confirm) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setConfirm(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}, [confirm]);
|
||||
return (
|
||||
<div ref={ref} className={`inline-block`}>
|
||||
<VoidButtonBgDarken className={className} onClick={() => {
|
||||
if (!confirm) {
|
||||
setConfirm(true);
|
||||
} else {
|
||||
onConfirm();
|
||||
setConfirm(false);
|
||||
}
|
||||
}}>
|
||||
{confirm ? `Confirm Reset` : children}
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// shows a providerName dropdown if no `providerName` is given
|
||||
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
|
||||
@@ -165,17 +196,17 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
|
||||
const [showCheckmark, setShowCheckmark] = useState(false)
|
||||
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const providerName = permanentProviderName ?? userChosenProviderName;
|
||||
|
||||
const [modelName, setModelName] = useState<string>('')
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
const numModels = settingsState.settingsOfProvider[providerName].models.length
|
||||
const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length
|
||||
|
||||
if (showCheckmark) {
|
||||
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
|
||||
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white dark:text-black px-3 py-1 rounded-sm ${className}`} />
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
@@ -198,59 +229,66 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
|
||||
<button onClick={() => { setIsOpen(false) }} className='text-void-fg-4'><X className='size-4' /></button> */}
|
||||
|
||||
{/* provider input */}
|
||||
{!permanentProviderName &&
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setUserChosenProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
|
||||
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
}
|
||||
<ErrorBoundary>
|
||||
{!permanentProviderName &&
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setUserChosenProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
|
||||
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
}
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* model input */}
|
||||
<VoidSimpleInputBox
|
||||
value={modelName}
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
compact={compact}
|
||||
className={'max-w-32'}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<VoidSimpleInputBox
|
||||
value={modelName}
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
compact={compact}
|
||||
className={'max-w-32'}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* add button */}
|
||||
<AddButton
|
||||
type='submit'
|
||||
disabled={!modelName}
|
||||
onClick={(e) => {
|
||||
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}.`)
|
||||
setErrorString(`This model already exists.`)
|
||||
return
|
||||
}
|
||||
<ErrorBoundary>
|
||||
<AddButton
|
||||
type='submit'
|
||||
disabled={!modelName}
|
||||
onClick={(e) => {
|
||||
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}.`)
|
||||
setErrorString(`This model already exists.`)
|
||||
return
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
setShowCheckmark(true)
|
||||
setTimeout(() => {
|
||||
setShowCheckmark(false)
|
||||
setIsOpen(false)
|
||||
}, 1500)
|
||||
setErrorString('')
|
||||
setModelName('')
|
||||
}}
|
||||
/>
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
setShowCheckmark(true)
|
||||
setTimeout(() => {
|
||||
setShowCheckmark(false)
|
||||
setIsOpen(false)
|
||||
}, 1500)
|
||||
setErrorString('')
|
||||
setModelName('')
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
</form>
|
||||
@@ -286,7 +324,7 @@ export const ModelDump = () => {
|
||||
|
||||
return <div className=''>
|
||||
{modelDump.map((m, i) => {
|
||||
const { isHidden, isDefault, isAutodetected, modelName, providerName, providerEnabled } = m
|
||||
const { isHidden, type, modelName, providerName, providerEnabled } = m
|
||||
|
||||
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
|
||||
|
||||
@@ -312,14 +350,13 @@ export const ModelDump = () => {
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className='flex items-center gap-4'
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content={disabled? `${displayInfoOfProviderName(providerName).title} is disabled`
|
||||
: (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
|
||||
|
||||
}
|
||||
// data-tooltip-id='void-tooltip'
|
||||
// data-tooltip-place='top'
|
||||
// data-tooltip-content={disabled ? `${displayInfoOfProviderName(providerName).title} is disabled`
|
||||
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
|
||||
// }
|
||||
>
|
||||
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
||||
<span className='opacity-50 truncate'>{type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'}</span>
|
||||
|
||||
<VoidSwitch
|
||||
value={value}
|
||||
@@ -333,7 +370,7 @@ export const ModelDump = () => {
|
||||
/>
|
||||
|
||||
<div className={`w-5 flex items-center justify-center`}>
|
||||
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
|
||||
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,9 +382,9 @@ export const ModelDump = () => {
|
||||
|
||||
// providers
|
||||
|
||||
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
|
||||
const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerName: ProviderName, settingName: SettingName, subTextMd: React.ReactNode }) => {
|
||||
|
||||
const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName)
|
||||
const { title: settingTitle, placeholder, isPasswordField } = displayInfoOfSettingName(providerName, settingName)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
@@ -371,10 +408,9 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
passwordBlur={isPasswordField}
|
||||
compact={true}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender string={subTextMd} chatMessageLocation={undefined} />
|
||||
{!subTextMd ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
{subTextMd}
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
@@ -457,7 +493,14 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi
|
||||
<div className='px-0'>
|
||||
{/* settings besides models (e.g. api key) */}
|
||||
{settingNames.map((settingName, i) => {
|
||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
|
||||
return <ProviderSetting
|
||||
key={settingName}
|
||||
providerName={providerName}
|
||||
settingName={settingName}
|
||||
subTextMd={i !== settingNames.length - 1 ? null
|
||||
: <ChatMarkdownRender string={subTextMdOfProviderName(providerName)} chatMessageLocation={undefined} />}
|
||||
/>
|
||||
})}
|
||||
|
||||
{showProviderSuggestions && needsModel ?
|
||||
@@ -546,13 +589,20 @@ const FastApplyMethodDropdown = () => {
|
||||
}
|
||||
|
||||
|
||||
export const ollamaSetupInstructions = <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
|
||||
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
export const OllamaSetupInstructions = () => {
|
||||
return <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
|
||||
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
|
||||
<div
|
||||
className='pl-6 flex items-center w-fit'
|
||||
data-tooltip-id='void-tooltip-ollama-settings'
|
||||
>
|
||||
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const RedoOnboardingButton = ({ className }: { className?: string }) => {
|
||||
@@ -568,170 +618,6 @@ const RedoOnboardingButton = ({ className }: { className?: string }) => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const FeaturesTab = () => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
|
||||
return <>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<ModelDump />
|
||||
<AddModelInputBox className='mt-4' compact />
|
||||
<RedoOnboardingButton className='mt-2 mb-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
{ollamaSetupInstructions}
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
|
||||
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
|
||||
<ErrorBoundary>
|
||||
{/* L1 */}
|
||||
<div className='flex items-start justify-around mt-4 my-4 gap-x-8'>
|
||||
{/* FIM */}
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Experimental. Only works with models that support FIM.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Enable Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.enableAutocomplete}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${!voidSettingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Apply */}
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Settings that control the behavior of the Apply button and the Edit tool.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.syncApplyToChat}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${voidSettingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Fast Apply Method Dropdown */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<FastApplyMethodDropdown />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* L2 */}
|
||||
<div className='flex items-start justify-around my-4 gap-x-8'>
|
||||
|
||||
{/* Tools Section */}
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Tools</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.autoApprove}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('autoApprove', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}</span>
|
||||
</div>
|
||||
|
||||
{/* Tool Lint Errors Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.includeToolLintErrors}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.includeToolLintErrors ? 'Include after-edit lint errors' : `Don't include lint errors`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div className='py-8' />
|
||||
|
||||
</ErrorBoundary>
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
|
||||
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
|
||||
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
|
||||
@@ -865,6 +751,34 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const ToolApprovalTypeSwitch = ({ approvalType, size, desc }: { approvalType: ToolApprovalType, size: "xxs" | "xs" | "sm" | "sm+" | "md", desc: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const onToggleAutoApprove = useCallback((approvalType: ToolApprovalType, newValue: boolean) => {
|
||||
voidSettingsService.setGlobalSetting('autoApprove', {
|
||||
...voidSettingsService.state.globalSettings.autoApprove,
|
||||
[approvalType]: newValue
|
||||
})
|
||||
metricsService.capture('Tool Auto-Accept Toggle', { enabled: newValue })
|
||||
}, [voidSettingsService, metricsService])
|
||||
|
||||
return <>
|
||||
<VoidSwitch
|
||||
size={size}
|
||||
value={voidSettingsState.globalSettings.autoApprove[approvalType] ?? false}
|
||||
onChange={(newVal) => onToggleAutoApprove(approvalType, newVal)}
|
||||
/>
|
||||
<span className="text-void-fg-3 text-xs">{desc}</span>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const fileService = accessor.get('IFileService')
|
||||
@@ -891,7 +805,28 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
|
||||
setTransferState({ type: 'loading' })
|
||||
|
||||
let errAcc = ''
|
||||
for (let { from, to } of transferTheseFiles) {
|
||||
// Define extensions to skip when transferring
|
||||
const extensionBlacklist = [
|
||||
// ignore extensions
|
||||
'ms-vscode-remote.remote-ssh',
|
||||
'ms-vscode-remote.remote-wsl',
|
||||
// ignore other AI copilots that could conflict with Void keybindings
|
||||
'sourcegraph.cody-ai',
|
||||
'continue.continue',
|
||||
'codeium.codeium',
|
||||
'saoudrizwan.claude-dev', // cline
|
||||
'rooveterinaryinc.roo-cline', // roo
|
||||
];
|
||||
for (const { from, to } of transferTheseFiles) {
|
||||
try {
|
||||
// find a blacklisted item
|
||||
const isBlacklisted = extensionBlacklist.find(blacklistItem => {
|
||||
return from.fsPath?.includes(blacklistItem)
|
||||
})
|
||||
if (isBlacklisted) continue
|
||||
|
||||
} catch { }
|
||||
|
||||
console.log('transferring', from, to)
|
||||
// Check if the source file exists before attempting to copy
|
||||
try {
|
||||
@@ -938,113 +873,358 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
|
||||
}
|
||||
|
||||
|
||||
const GeneralTab = () => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const environmentService = accessor.get('IEnvironmentService')
|
||||
const nativeHostService = accessor.get('INativeHostService')
|
||||
|
||||
return <>
|
||||
<div className=''>
|
||||
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`Instructions to include on all AI requests.`}</h4>
|
||||
<AIInstructionsBox />
|
||||
</div>
|
||||
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
// full settings
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark()
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const environmentService = accessor.get('IEnvironmentService')
|
||||
const nativeHostService = accessor.get('INativeHostService')
|
||||
const settingsState = useSettingsState()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const notificationService = accessor.get('INotificationService')
|
||||
|
||||
const onDownload = (t: 'Chats' | 'Settings') => {
|
||||
let dataStr: string
|
||||
let downloadName: string
|
||||
if (t === 'Chats') {
|
||||
// Export chat threads
|
||||
dataStr = JSON.stringify(chatThreadsService.state, null, 2)
|
||||
downloadName = 'void-chats.json'
|
||||
}
|
||||
else if (t === 'Settings') {
|
||||
// Export user settings
|
||||
dataStr = JSON.stringify(voidSettingsService.state, null, 2)
|
||||
downloadName = 'void-settings.json'
|
||||
}
|
||||
else {
|
||||
dataStr = ''
|
||||
downloadName = ''
|
||||
}
|
||||
|
||||
const blob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = downloadName
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
|
||||
// Add file input refs
|
||||
const fileInputSettingsRef = useRef<HTMLInputElement>(null)
|
||||
const fileInputChatsRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [s, ss] = useState(0)
|
||||
|
||||
const handleUpload = (t: 'Chats' | 'Settings') => (e: React.ChangeEvent<HTMLInputElement>,) => {
|
||||
const files = e.target.files
|
||||
if (!files) return;
|
||||
const file = files[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const json = JSON.parse(reader.result as string);
|
||||
|
||||
if (t === 'Chats') {
|
||||
chatThreadsService.dangerousSetState(json as any)
|
||||
}
|
||||
else if (t === 'Settings') {
|
||||
voidSettingsService.dangerousSetState(json as any)
|
||||
}
|
||||
|
||||
notificationService.info(`${t} imported successfully!`)
|
||||
} catch (err) {
|
||||
notificationService.notify({ message: `Failed to import ${t}`, source: err + '', severity: Severity.Error, })
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = '';
|
||||
|
||||
ss(s => s + 1)
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<TabName>('models')
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
|
||||
<div className='max-w-5xl mx-auto'>
|
||||
<div className='max-w-xl mx-auto'>
|
||||
|
||||
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
|
||||
|
||||
{/* separator */}
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
<div className='flex items-stretch'>
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
|
||||
{/* tabs */}
|
||||
<div className='flex flex-col w-full max-w-32'>
|
||||
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('models') }}
|
||||
>Models</button>
|
||||
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'general' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('general') }}
|
||||
>General</button>
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ModelDump />
|
||||
<AddModelInputBox className='mt-4' compact />
|
||||
<RedoOnboardingButton className='mt-2 mb-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
<OllamaSetupInstructions />
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
|
||||
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
|
||||
{/* L1 */}
|
||||
|
||||
<div className='flex items-start justify-around my-4 gap-x-8'>
|
||||
<ErrorBoundary>
|
||||
{/* FIM */}
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>
|
||||
<span>
|
||||
Experimental.{' '}
|
||||
</span>
|
||||
<span
|
||||
className='hover:brightness-110'
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content='We recommend using qwen2.5-coder:1.5b with Ollama.'
|
||||
data-tooltip-class-name='void-max-w-[20px]'
|
||||
>
|
||||
Only works with FIM models.*
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Enable Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.enableAutocomplete}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<ErrorBoundary>
|
||||
<div className={`my-2 ${!settingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Apply */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Settings that control the behavior of the Apply button and the Edit tool.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.syncApplyToChat}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${settingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Fast Apply Method Dropdown */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<FastApplyMethodDropdown />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* L2 */}
|
||||
|
||||
<div className='flex items-start justify-around my-4 gap-x-8'>
|
||||
|
||||
{/* Tools Section */}
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Tools</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
{[...toolApprovalTypes].map((approvalType) => {
|
||||
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
</div>
|
||||
})}
|
||||
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Tool Lint Errors Switch */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.includeToolLintErrors}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Fix lint errors`}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* separator */}
|
||||
<div className='w-[1px] mx-4' />
|
||||
|
||||
|
||||
{/* content */}
|
||||
<div className='w-full min-w-[550px]'>
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
|
||||
|
||||
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
|
||||
<FeaturesTab />
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'general' ? 'hidden' : ''}`}>
|
||||
<GeneralTab />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* General section (formerly GeneralTab) */}
|
||||
<div className='mt-12'>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Import/Export section, as its own block right after One-Click Switch */}
|
||||
<div className='mt-12'>
|
||||
<h2 className='text-3xl mb-2'>Import/Export</h2>
|
||||
<div className='flex gap-8'>
|
||||
{/* Settings Subcategory */}
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<h3 className='text-xl mb-2'>Settings</h3>
|
||||
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
|
||||
Import Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
|
||||
Export Settings
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
|
||||
Reset Settings
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
{/* Chats Subcategory */}
|
||||
<div className='flex flex-col gap-2 w-full max-w-48'>
|
||||
<h3 className='text-xl mb-2'>Chat</h3>
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Import Chats
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
|
||||
Export Chats
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
|
||||
Reset Chats
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<ErrorBoundary>
|
||||
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
System instructions to include with all AI requests.
|
||||
Alternatively, place a \`.voidrules\` file in the root of your workspace.
|
||||
`} chatMessageLocation={undefined} />
|
||||
</h4>
|
||||
<ErrorBoundary>
|
||||
<AIInstructionsBox />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const WarningBox = ({ text, onClick, className }: { text: string; onClick
|
||||
>
|
||||
<IconWarning
|
||||
size={14}
|
||||
className='mr-1'
|
||||
className='mr-1 flex-shrink-0'
|
||||
/>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import '../styles.css'
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
import { useIsDark } from '../util/services.js';
|
||||
@@ -44,7 +45,7 @@ export const VoidTooltip = () => {
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
#void-tooltip, #void-tooltip-orange, #void-tooltip-green {
|
||||
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings {
|
||||
font-size: 12px;
|
||||
padding: 0px 8px;
|
||||
border-radius: 6px;
|
||||
@@ -66,6 +67,11 @@ export const VoidTooltip = () => {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#void-tooltip-ollama-settings {
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
}
|
||||
|
||||
.react-tooltip-arrow {
|
||||
z-index: -1 !important; /* Keep arrow behind content (somehow this isnt done automatically) */
|
||||
}
|
||||
@@ -92,6 +98,28 @@ export const VoidTooltip = () => {
|
||||
opacity={1}
|
||||
delayShow={50}
|
||||
/>
|
||||
<Tooltip
|
||||
id="void-tooltip-ollama-settings"
|
||||
border='1px solid rgba(100,100,100,.2)'
|
||||
opacity={1}
|
||||
openEvents={{ mouseover: true, click: true, focus: true }}
|
||||
place='right'
|
||||
style={{ pointerEvents: 'all', userSelect: 'text', fontSize: 11 }}
|
||||
>
|
||||
<div style={{ padding: '8px 10px' }}>
|
||||
<div style={{ opacity: 0.8, textAlign: 'center', fontWeight: 'bold', marginBottom: 8 }}>
|
||||
Good starter models
|
||||
</div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<span style={{ opacity: 0.8 }}>For chat:{` `}</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>llama3.1</span>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ opacity: 0.8 }}>For autocomplete:{` `}</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder:1.5b</span>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,6 +53,7 @@ module.exports = {
|
||||
'void-bg-1-alt': 'var(--void-bg-1-alt)',
|
||||
'void-bg-2': 'var(--void-bg-2)',
|
||||
'void-bg-2-alt': 'var(--void-bg-2-alt)',
|
||||
'void-bg-2-hover': 'var(--void-bg-2-hover)',
|
||||
'void-bg-3': 'var(--void-bg-3)',
|
||||
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
|
||||
|
||||
|
||||
export interface ISearchReplaceService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
|
||||
export class SearchReplaceService extends Disposable implements ISearchReplaceService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
constructor(
|
||||
// @ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
// send(params: ServiceSendLLMMessageParams & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
|
||||
// this.llmMessageService.sendLLMMessage({
|
||||
// ...params as ServiceSendLLMMessageParams,
|
||||
// onText: (p) => {
|
||||
// const { retry } = params.onText(p)
|
||||
// if (retry) {
|
||||
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ISearchReplaceService, SearchReplaceService, InstantiationType.Eager);
|
||||
@@ -25,10 +25,37 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
|
||||
import { IChatThreadService } from './chatThreadService.js';
|
||||
import { getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
const s = currentSelections[i]
|
||||
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
|
||||
if (s.type === 'File' && newSelection.type === 'File') {
|
||||
return i
|
||||
}
|
||||
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
// if there's any collision return true
|
||||
const [oldStart, oldEnd] = s.range
|
||||
const [newStart, newEnd] = newSelection.range
|
||||
if (oldStart !== newStart || oldEnd !== newEnd) continue
|
||||
return i
|
||||
}
|
||||
if (s.type === 'Folder' && newSelection.type === 'Folder') {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
|
||||
if (!range)
|
||||
return null
|
||||
@@ -63,31 +90,6 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e
|
||||
// }
|
||||
|
||||
|
||||
const findStagingItemToReplace = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): [number, StagingSelectionItem] | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
const s = currentSelections[i]
|
||||
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
|
||||
if (s.type === 'File' && newSelection.type === 'File') {
|
||||
return [i, s] as const
|
||||
}
|
||||
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
// if there's any collision return true
|
||||
const [oldStart, oldEnd] = s.range
|
||||
const [newStart, newEnd] = newSelection.range
|
||||
if (oldStart !== newStart || oldEnd !== newEnd) continue
|
||||
return [i, s] as const
|
||||
}
|
||||
if (s.type === 'Folder' && newSelection.type === 'Folder') {
|
||||
return [i, s] as const
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
registerAction2(class extends Action2 {
|
||||
@@ -132,7 +134,7 @@ registerAction2(class extends Action2 {
|
||||
}
|
||||
|
||||
|
||||
const selection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
@@ -163,21 +165,17 @@ registerAction2(class extends Action2 {
|
||||
}
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const replaceRes = findStagingItemToReplace(selections, selection)
|
||||
if (replaceRes) {
|
||||
const [idx, newSel] = replaceRes
|
||||
|
||||
if (idx !== undefined && idx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, idx),
|
||||
newSel,
|
||||
...selections!.slice(idx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
const idx = findStagingSelectionIndex(selections, newSelection)
|
||||
if (idx !== null && idx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, idx),
|
||||
newSelection,
|
||||
...selections!.slice(idx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add it
|
||||
else {
|
||||
setSelections([...(selections ?? []), selection])
|
||||
setSelections([...(selections ?? []), newSelection])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -204,7 +202,19 @@ registerAction2(class extends Action2 {
|
||||
})
|
||||
|
||||
|
||||
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
|
||||
// focus
|
||||
stateService.fireFocusChat()
|
||||
const window = getActiveWindow()
|
||||
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
||||
|
||||
}
|
||||
|
||||
|
||||
// New chat menu button
|
||||
@@ -215,6 +225,25 @@ registerAction2(class extends Action2 {
|
||||
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 metricsService = accessor.get(IMetricsService)
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
||||
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
// New chat keybind
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.newChatKeybindAction',
|
||||
title: 'New Chat Keybind',
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
@@ -222,15 +251,16 @@ registerAction2(class extends Action2 {
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const commandService = accessor.get(ICommandService)
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
|
||||
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
// add user's selection to chat
|
||||
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -245,13 +275,27 @@ registerAction2(class extends Action2 {
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
// do not do anything if there are no messages (without this it clears all of the user's selections if the button is pressed)
|
||||
// TODO the history button should be disabled in this case so we can remove this logic
|
||||
const thread = accessor.get(IChatThreadService).getCurrentThread()
|
||||
if (thread.messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
|
||||
metricsService.capture('Chat Navigation', { type: 'History' })
|
||||
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
// doesnt do anything right now
|
||||
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
|
||||
stateService.fireBlurChat()
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
|
||||
|
||||
// service that manages sidebar's state
|
||||
export type VoidSidebarState = {
|
||||
isHistoryOpen: boolean;
|
||||
isHistoryOpen: boolean; // this isn't doing anything right now
|
||||
currentTab: 'chat';
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
|
||||
import { TerminalExitReason, TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
|
||||
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
|
||||
import { MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
|
||||
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
|
||||
import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js';
|
||||
|
||||
|
||||
|
||||
@@ -18,9 +18,12 @@ export interface ITerminalToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
listTerminalIds(): string[];
|
||||
runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>;
|
||||
openTerminal(terminalId: string): Promise<void>
|
||||
runCommand(command: string, bgTerminalId: string | null): Promise<{ terminalId: string, resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
|
||||
focusTerminal(terminalId: string): Promise<void>
|
||||
terminalExists(terminalId: string): boolean
|
||||
|
||||
createTerminal(): Promise<string>
|
||||
killTerminal(terminalId: string): Promise<void>
|
||||
}
|
||||
|
||||
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
|
||||
@@ -103,12 +106,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
throw new Error('This should never be reached by pigeonhole principle');
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async _getOrCreateTerminal(proposedTerminalId: string) {
|
||||
// if terminal ID exists, return it
|
||||
if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false }
|
||||
|
||||
async createTerminal() {
|
||||
// create new terminal and return its ID
|
||||
const terminalId = this.getValidNewTerminalId();
|
||||
const terminal = await this.terminalService.createTerminal({
|
||||
@@ -127,13 +125,21 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
})
|
||||
disposables.push(d)
|
||||
})
|
||||
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 1000) })
|
||||
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
|
||||
|
||||
await Promise.any([waitForMount, waitForTimeout,])
|
||||
disposables.forEach(d => d.dispose())
|
||||
|
||||
this.terminalInstanceOfId[terminalId] = terminal
|
||||
return { terminalId, didCreateTerminal: true }
|
||||
return terminalId
|
||||
}
|
||||
|
||||
async killTerminal(terminalId: string) {
|
||||
const terminal = this.terminalInstanceOfId[terminalId]
|
||||
if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} did not exist.`);
|
||||
terminal.dispose(TerminalExitReason.Extension)
|
||||
delete this.terminalInstanceOfId[terminalId]
|
||||
return
|
||||
}
|
||||
|
||||
terminalExists(terminalId: string): boolean {
|
||||
@@ -141,7 +147,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
}
|
||||
|
||||
|
||||
openTerminal: ITerminalToolService['openTerminal'] = async (terminalId) => {
|
||||
focusTerminal: ITerminalToolService['focusTerminal'] = async (terminalId) => {
|
||||
if (!terminalId) return
|
||||
const terminal = this.terminalInstanceOfId[terminalId]
|
||||
if (!terminal) return // should never happen
|
||||
@@ -151,81 +157,104 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
|
||||
|
||||
|
||||
runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => {
|
||||
|
||||
runCommand: ITerminalToolService['runCommand'] = async (command, bgTerminalId) => {
|
||||
await this.terminalService.whenConnected;
|
||||
const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId)
|
||||
const terminal = this.terminalInstanceOfId[terminalId];
|
||||
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`);
|
||||
|
||||
// focus the terminal about to run
|
||||
this.terminalService.setActiveInstance(terminal)
|
||||
await this.terminalService.focusActiveInstance()
|
||||
|
||||
let result: string = ''
|
||||
let resolveReason: TerminalResolveReason | undefined = undefined
|
||||
|
||||
let terminal: ITerminalInstance
|
||||
const disposables: IDisposable[] = []
|
||||
|
||||
const waitUntilDone = new Promise<void>((res, rej) => {
|
||||
const d2 = terminal.onData(async newData => {
|
||||
if (resolveReason) return
|
||||
const isBG = bgTerminalId !== null
|
||||
let terminalId: string
|
||||
if (isBG) { // BG process
|
||||
terminal = this.terminalInstanceOfId[bgTerminalId];
|
||||
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${bgTerminalId} did not exist.`);
|
||||
terminalId = bgTerminalId
|
||||
}
|
||||
else {
|
||||
terminalId = await this.createTerminal()
|
||||
terminal = this.terminalInstanceOfId[terminalId]
|
||||
if (!terminal) throw new Error(`Unexpected error: Terminal could not be created.`)
|
||||
}
|
||||
|
||||
result += newData
|
||||
|
||||
// onPageFull
|
||||
if (result.length > MAX_TERMINAL_CHARS_PAGE) {
|
||||
result = result.substring(0, MAX_TERMINAL_CHARS_PAGE)
|
||||
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
|
||||
resolveReason = { type: 'toofull' }
|
||||
res()
|
||||
return
|
||||
}
|
||||
const waitForResult = async () => {
|
||||
// focus the terminal about to run
|
||||
this.terminalService.setActiveInstance(terminal)
|
||||
await this.terminalService.focusActiveInstance()
|
||||
|
||||
// onDone
|
||||
const isDone = isCommandComplete(result)
|
||||
if (isDone) {
|
||||
resolveReason = { type: 'done', exitCode: isDone.exitCode }
|
||||
res()
|
||||
return
|
||||
}
|
||||
let result: string = ''
|
||||
let resolveReason: TerminalResolveReason | undefined = undefined
|
||||
|
||||
|
||||
// create this before we send so that we don't miss events on terminal
|
||||
const waitUntilDone = new Promise<void>((res, rej) => {
|
||||
const d2 = terminal.onData(async newData => {
|
||||
if (resolveReason) return
|
||||
result += newData
|
||||
// onDone
|
||||
const isDone = isCommandComplete(result)
|
||||
if (isDone) {
|
||||
resolveReason = { type: 'done', exitCode: isDone.exitCode }
|
||||
res()
|
||||
return
|
||||
}
|
||||
})
|
||||
disposables.push(d2)
|
||||
})
|
||||
disposables.push(d2)
|
||||
})
|
||||
|
||||
|
||||
// send the command here
|
||||
await terminal.sendText(command, true)
|
||||
// send the command here
|
||||
await terminal.sendText(command, true)
|
||||
|
||||
// timeout promise
|
||||
const waitUntilTimeout = new Promise<void>((res, rej) => {
|
||||
setTimeout(async () => {
|
||||
if (resolveReason) return
|
||||
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
|
||||
resolveReason = { type: waitForCompletion ? 'timeout' : 'bgtask' }
|
||||
res()
|
||||
return
|
||||
}, (waitForCompletion ? TERMINAL_TIMEOUT_TIME : TERMINAL_BG_WAIT_TIME) * 1000)
|
||||
})
|
||||
// inactivity-based timeout
|
||||
const waitUntilInactive = new Promise<void>(res => {
|
||||
let globalTimeoutId: ReturnType<typeof setTimeout>;
|
||||
const resetTimer = () => {
|
||||
clearTimeout(globalTimeoutId);
|
||||
globalTimeoutId = setTimeout(() => {
|
||||
if (resolveReason) return
|
||||
|
||||
await Promise.any([
|
||||
waitUntilDone,
|
||||
waitUntilTimeout,
|
||||
])
|
||||
resolveReason = { type: 'timeout' };
|
||||
res();
|
||||
}, MAX_TERMINAL_INACTIVE_TIME * 1000);
|
||||
};
|
||||
|
||||
disposables.forEach(d => d.dispose())
|
||||
const dTimeout = terminal.onData(() => { resetTimer(); });
|
||||
disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId)));
|
||||
resetTimer();
|
||||
});
|
||||
|
||||
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
||||
// wait for result
|
||||
await Promise.any([waitUntilDone, waitUntilInactive,])
|
||||
|
||||
disposables.forEach(d => d.dispose())
|
||||
if (!isBG) {
|
||||
await this.killTerminal(terminalId)
|
||||
}
|
||||
|
||||
result = removeAnsiEscapeCodes(result)
|
||||
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
|
||||
.join('\n')
|
||||
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
||||
|
||||
return { terminalId, didCreateTerminal, result, resolveReason }
|
||||
result = removeAnsiEscapeCodes(result)
|
||||
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
|
||||
.join('\n')
|
||||
|
||||
if (result.length > MAX_TERMINAL_CHARS) {
|
||||
const half = MAX_TERMINAL_CHARS / 2
|
||||
result = result.slice(0, half)
|
||||
+ '\n...\n'
|
||||
+ result.slice(result.length - half, Infinity)
|
||||
}
|
||||
|
||||
return { result, resolveReason }
|
||||
|
||||
}
|
||||
const resPromise = waitForResult()
|
||||
|
||||
return { terminalId, resPromise }
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed);
|
||||
|
||||
@@ -14,10 +14,10 @@ import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
import { basename } from '../../../../base/common/path.js'
|
||||
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
||||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
|
||||
import { IMarkerService } from '../../../../platform/markers/common/markers.js'
|
||||
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
|
||||
import { timeout } from '../../../../base/common/async.js'
|
||||
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
|
||||
import { ToolName } from '../common/prompt/prompts.js'
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
|
||||
|
||||
@@ -27,20 +27,11 @@ import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
||||
|
||||
|
||||
|
||||
|
||||
// pagination info
|
||||
export const MAX_FILE_CHARS_PAGE = 50_000
|
||||
export const MAX_CHILDREN_URIs_PAGE = 500
|
||||
export const MAX_TERMINAL_CHARS_PAGE = 20_000
|
||||
export const TERMINAL_TIMEOUT_TIME = 5 // seconds
|
||||
export const TERMINAL_BG_WAIT_TIME = 1
|
||||
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
return !u || u === 'null' || u === 'undefined'
|
||||
}
|
||||
@@ -163,8 +154,11 @@ export class ToolsService implements IToolsService {
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const startLine = validateNumber(startLineUnknown, { default: null })
|
||||
const endLine = validateNumber(endLineUnknown, { default: null })
|
||||
let startLine = validateNumber(startLineUnknown, { default: null })
|
||||
let endLine = validateNumber(endLineUnknown, { default: null })
|
||||
|
||||
if (startLine !== null && startLine < 1) startLine = null
|
||||
if (endLine !== null && endLine < 1) endLine = null
|
||||
|
||||
return { uri, startLine, endLine, pageNumber }
|
||||
},
|
||||
@@ -173,12 +167,12 @@ export class ToolsService implements IToolsService {
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
return { rootURI: uri, pageNumber }
|
||||
return { uri, pageNumber }
|
||||
},
|
||||
get_dir_structure: (params: RawToolParamsObj) => {
|
||||
get_dir_tree: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, } = params
|
||||
const uri = validateURI(uriStr)
|
||||
return { rootURI: uri }
|
||||
return { uri }
|
||||
},
|
||||
search_pathnames_only: (params: RawToolParamsObj) => {
|
||||
const {
|
||||
@@ -189,26 +183,35 @@ export class ToolsService implements IToolsService {
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
const searchInFolder = validateOptionalStr('search_in_folder', includeUnknown)
|
||||
const includePattern = validateOptionalStr('include_pattern', includeUnknown)
|
||||
|
||||
return { queryStr, searchInFolder, pageNumber }
|
||||
return { query: queryStr, includePattern, pageNumber }
|
||||
|
||||
},
|
||||
search_files: (params: RawToolParamsObj) => {
|
||||
search_for_files: (params: RawToolParamsObj) => {
|
||||
const {
|
||||
query: queryUnknown,
|
||||
search_in_folder: searchInFolderUnknown,
|
||||
is_regex: isRegexUnknown,
|
||||
page_number: pageNumberUnknown
|
||||
} = params
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const searchInFolder = validateOptionalURI(searchInFolderUnknown)
|
||||
const isRegex = validateBoolean(isRegexUnknown, { default: false })
|
||||
|
||||
return { queryStr, searchInFolder, isRegex, pageNumber }
|
||||
return {
|
||||
query: queryStr,
|
||||
isRegex,
|
||||
searchInFolder,
|
||||
pageNumber
|
||||
}
|
||||
},
|
||||
search_in_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, query: queryUnknown, is_regex: isRegexUnknown } = params;
|
||||
const uri = validateURI(uriStr);
|
||||
const query = validateStr('query', queryUnknown);
|
||||
const isRegex = validateBoolean(isRegexUnknown, { default: false });
|
||||
return { uri, query, isRegex };
|
||||
},
|
||||
|
||||
read_lint_errors: (params: RawToolParamsObj) => {
|
||||
@@ -239,18 +242,28 @@ export class ToolsService implements IToolsService {
|
||||
},
|
||||
|
||||
edit_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, change_description: changeDescriptionUnknown } = params
|
||||
const { uri: uriStr, change_diff: changeDiffUnknown } = params
|
||||
const uri = validateURI(uriStr)
|
||||
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
|
||||
return { uri, changeDescription }
|
||||
const changeDiff = validateStr('changeDiff', changeDiffUnknown)
|
||||
return { uri, changeDiff }
|
||||
},
|
||||
|
||||
run_terminal_command: (params: RawToolParamsObj) => {
|
||||
const { command: commandUnknown, terminal_id: terminalIdUnknown, wait_for_completion: waitForCompletionUnknown } = params
|
||||
const command = validateStr('command', commandUnknown)
|
||||
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
|
||||
const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true })
|
||||
return { command, proposedTerminalId, waitForCompletion }
|
||||
// ---
|
||||
|
||||
run_command: (params: RawToolParamsObj) => {
|
||||
const { command: commandUnknown, terminal_id: terminalIdUnknown } = params;
|
||||
const command = validateStr('command', commandUnknown);
|
||||
const proposedTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null;
|
||||
return { command, bgTerminalId: proposedTerminalId };
|
||||
},
|
||||
open_persistent_terminal: (_params: RawToolParamsObj) => {
|
||||
// No parameters needed; will open a new background terminal
|
||||
return {};
|
||||
},
|
||||
kill_persistent_terminal: (params: RawToolParamsObj) => {
|
||||
const { terminal_id: terminalIdUnknown } = params;
|
||||
const terminalId = validateProposedTerminalId(terminalIdUnknown);
|
||||
return { terminalId };
|
||||
},
|
||||
|
||||
}
|
||||
@@ -260,7 +273,7 @@ export class ToolsService implements IToolsService {
|
||||
read_file: async ({ uri, startLine, endLine, pageNumber }) => {
|
||||
await voidModelService.initializeModel(uri)
|
||||
const { model } = await voidModelService.getModelSafe(uri)
|
||||
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
|
||||
if (model === null) { throw new Error(`No contents; File does not exist.`) }
|
||||
|
||||
let contents: string
|
||||
if (startLine === null && endLine === null) {
|
||||
@@ -276,25 +289,25 @@ export class ToolsService implements IToolsService {
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const hasNextPage = (contents.length - 1) - toIdx >= 1
|
||||
|
||||
return { result: { fileContents, hasNextPage } }
|
||||
const totalFileLen = contents.length
|
||||
return { result: { fileContents, totalFileLen, hasNextPage } }
|
||||
},
|
||||
|
||||
ls_dir: async ({ rootURI, pageNumber }) => {
|
||||
const dirResult = await computeDirectoryTree1Deep(fileService, rootURI, pageNumber)
|
||||
ls_dir: async ({ uri, pageNumber }) => {
|
||||
const dirResult = await computeDirectoryTree1Deep(fileService, uri, pageNumber)
|
||||
return { result: dirResult }
|
||||
},
|
||||
|
||||
get_dir_structure: async ({ rootURI }) => {
|
||||
const str = await this.directoryStrService.getDirectoryStrTool(rootURI)
|
||||
get_dir_tree: async ({ uri }) => {
|
||||
const str = await this.directoryStrService.getDirectoryStrTool(uri)
|
||||
return { result: { str } }
|
||||
},
|
||||
|
||||
search_pathnames_only: async ({ queryStr, searchInFolder, pageNumber }) => {
|
||||
search_pathnames_only: async ({ query: queryStr, includePattern, pageNumber }) => {
|
||||
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
|
||||
filePattern: queryStr,
|
||||
includePattern: searchInFolder ?? undefined,
|
||||
includePattern: includePattern ?? undefined,
|
||||
})
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
@@ -308,7 +321,7 @@ export class ToolsService implements IToolsService {
|
||||
return { result: { uris, hasNextPage } }
|
||||
},
|
||||
|
||||
search_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
|
||||
search_for_files: async ({ query: queryStr, isRegex, searchInFolder, pageNumber }) => {
|
||||
const searchFolders = searchInFolder === null ?
|
||||
workspaceContextService.getWorkspace().folders.map(f => f.uri)
|
||||
: [searchInFolder]
|
||||
@@ -329,6 +342,24 @@ export class ToolsService implements IToolsService {
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
return { result: { queryStr, uris, hasNextPage } }
|
||||
},
|
||||
search_in_file: async ({ uri, query, isRegex }) => {
|
||||
await voidModelService.initializeModel(uri);
|
||||
const { model } = await voidModelService.getModelSafe(uri);
|
||||
if (model === null) { throw new Error(`No contents; File does not exist.`); }
|
||||
const contents = model.getValue(EndOfLinePreference.LF);
|
||||
const contentOfLine = contents.split('\n');
|
||||
const totalLines = contentOfLine.length;
|
||||
const regex = isRegex ? new RegExp(query) : null;
|
||||
const lines: number[] = []
|
||||
for (let i = 0; i < totalLines; i++) {
|
||||
const line = contentOfLine[i];
|
||||
if ((isRegex && regex!.test(line)) || (!isRegex && line.includes(query))) {
|
||||
const matchLine = i + 1;
|
||||
lines.push(matchLine);
|
||||
}
|
||||
}
|
||||
return { result: { lines } };
|
||||
},
|
||||
|
||||
read_lint_errors: async ({ uri }) => {
|
||||
await timeout(1000)
|
||||
@@ -352,14 +383,14 @@ export class ToolsService implements IToolsService {
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
edit_file: async ({ uri, changeDescription }) => {
|
||||
edit_file: async ({ uri, changeDiff }) => {
|
||||
await voidModelService.initializeModel(uri)
|
||||
if (this.commandBarService.getStreamState(uri) === 'streaming') {
|
||||
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and resume later.`)
|
||||
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
|
||||
}
|
||||
const opts = {
|
||||
uri,
|
||||
applyStr: changeDescription,
|
||||
applyStr: changeDiff,
|
||||
from: 'ClickApply',
|
||||
startBehavior: 'keep-conflicts',
|
||||
} as const
|
||||
@@ -382,10 +413,25 @@ export class ToolsService implements IToolsService {
|
||||
|
||||
return { result: lintErrorsPromise, interruptTool }
|
||||
},
|
||||
run_terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
|
||||
const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
|
||||
return { result: { terminalId, didCreateTerminal, result, resolveReason } }
|
||||
// ---
|
||||
run_command: async ({ command, bgTerminalId }) => {
|
||||
const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, bgTerminalId)
|
||||
const interruptTool = () => {
|
||||
this.terminalToolService.killTerminal(terminalId)
|
||||
}
|
||||
return { result: resPromise, interruptTool }
|
||||
},
|
||||
open_persistent_terminal: async () => {
|
||||
// Open a new background terminal without waiting for completion
|
||||
const terminalId = await this.terminalToolService.createTerminal()
|
||||
return { result: { terminalId } }
|
||||
},
|
||||
kill_persistent_terminal: async ({ terminalId }) => {
|
||||
// Close the background terminal by sending exit
|
||||
await this.terminalToolService.killTerminal(terminalId)
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -395,26 +441,36 @@ export class ToolsService implements IToolsService {
|
||||
return lintErrors
|
||||
.map((e, i) => `Error ${i + 1}:\nLines Affected: ${e.startLineNumber}-${e.endLineNumber}\nError message:${e.message}`)
|
||||
.join('\n\n')
|
||||
.substring(0, MAX_FILE_CHARS_PAGE)
|
||||
}
|
||||
|
||||
// given to the LLM after the call
|
||||
// given to the LLM after the call for successful tool calls
|
||||
this.stringOfResult = {
|
||||
read_file: (params, result) => {
|
||||
return result.fileContents + nextPageStr(result.hasNextPage)
|
||||
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
|
||||
},
|
||||
ls_dir: (params, result) => {
|
||||
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
|
||||
return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining
|
||||
},
|
||||
get_dir_structure: (params, result) => {
|
||||
get_dir_tree: (params, result) => {
|
||||
return result.str
|
||||
},
|
||||
search_pathnames_only: (params, result) => {
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
search_files: (params, result) => {
|
||||
search_for_files: (params, result) => {
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
search_in_file: (params, result) => {
|
||||
const { model } = voidModelService.getModel(params.uri)
|
||||
if (!model) return '<Error getting string of result>'
|
||||
const lines = result.lines.map(n => {
|
||||
const lineContent = model.getValueInRange({ startLineNumber: n, startColumn: 1, endLineNumber: n, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
|
||||
return `Line ${n}:\n\`\`\`\n${lineContent}\n\`\`\``
|
||||
}).join('\n\n');
|
||||
return lines;
|
||||
},
|
||||
read_lint_errors: (params, result) => {
|
||||
return result.lintErrors ?
|
||||
stringifyLintErrors(result.lintErrors)
|
||||
@@ -436,30 +492,41 @@ export class ToolsService implements IToolsService {
|
||||
|
||||
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
|
||||
},
|
||||
run_terminal_command: (params, result) => {
|
||||
run_command: (params, result) => {
|
||||
const {
|
||||
terminalId,
|
||||
didCreateTerminal,
|
||||
resolveReason,
|
||||
result: result_,
|
||||
} = result
|
||||
const { bgTerminalId } = params
|
||||
|
||||
const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}`
|
||||
// success
|
||||
if (resolveReason.type === 'done') {
|
||||
const desc = bgTerminalId ? ` in terminal ${bgTerminalId}` : ''
|
||||
return `Terminal command executed and finished${desc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
|
||||
}
|
||||
|
||||
if (resolveReason.type === 'timeout') {
|
||||
return `Terminal command ran in ${terminalDesc}, but did not complete after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
|
||||
// bg command
|
||||
if (bgTerminalId !== null) {
|
||||
if (resolveReason.type === 'timeout') {
|
||||
return `Terminal command is running in the background in terminal ${bgTerminalId}. Here were the outputs after ${MAX_TERMINAL_INACTIVE_TIME} seconds:\n${result_}`
|
||||
}
|
||||
}
|
||||
else if (resolveReason.type === 'bgtask') {
|
||||
return `Terminal command is running in the background in ${terminalDesc}. Here were the outputs after ${TERMINAL_BG_WAIT_TIME} seconds:\n${result_}`
|
||||
}
|
||||
else if (resolveReason.type === 'toofull') {
|
||||
return `Terminal command executed in terminal ${terminalDesc}. Command was interrupted because output was too long. Result:\n${result_}`
|
||||
}
|
||||
else if (resolveReason.type === 'done') {
|
||||
return `Terminal command executed in terminal ${terminalDesc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
|
||||
// normal command
|
||||
else {
|
||||
if (resolveReason.type === 'timeout') {
|
||||
return `Terminal command ran, but was interrupted after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. Full output:\n${result_}`
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
|
||||
},
|
||||
open_persistent_terminal: (_params, result) => {
|
||||
const { terminalId } = result;
|
||||
return `Successfully created background terminal with ID ${terminalId}`;
|
||||
},
|
||||
kill_persistent_terminal: (params, _result) => {
|
||||
return `Successfully closed terminal ${params.terminalId}.`;
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@@ -471,9 +538,11 @@ export class ToolsService implements IToolsService {
|
||||
private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } {
|
||||
const lintErrors = this.markerService
|
||||
.read({ resource: uri })
|
||||
.filter(l => l.severity === MarkerSeverity.Error || l.severity === MarkerSeverity.Warning)
|
||||
.slice(0, 100)
|
||||
.map(l => ({
|
||||
code: typeof l.code === 'string' ? l.code : l.code?.value || '',
|
||||
message: l.message,
|
||||
message: (l.severity === MarkerSeverity.Error ? '(error) ' : '(warning) ') + l.message,
|
||||
startLineNumber: l.startLineNumber,
|
||||
endLineNumber: l.endLineNumber,
|
||||
} satisfies LintErrorItem))
|
||||
|
||||
@@ -32,6 +32,7 @@ import './media/void.css'
|
||||
// update (frontend part, also see platform/)
|
||||
import './voidUpdateActions.js'
|
||||
|
||||
import './convertToLLMMessageWorkbenchContrib.js'
|
||||
|
||||
// tools
|
||||
import './toolsService.js'
|
||||
|
||||
@@ -362,8 +362,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
||||
|
||||
registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationType.Delayed); // delayed is needed here :(
|
||||
|
||||
// registerWorkbenchContribution2(VoidCommandBarService.ID, VoidCommandBarService, WorkbenchPhase.BlockRestore);
|
||||
|
||||
|
||||
export type VoidCommandBarProps = {
|
||||
uri: URI | null;
|
||||
|
||||
@@ -32,15 +32,12 @@ export class OnboardingContribution extends Disposable implements IWorkbenchCont
|
||||
|
||||
const onboardingContainer = h('div.void-onboarding-container').root;
|
||||
workbench.appendChild(onboardingContainer);
|
||||
|
||||
// Mount the React component
|
||||
this.instantiationService.invokeFunction((accessor: ServicesAccessor) => {
|
||||
const result = mountVoidOnboarding(onboardingContainer, accessor);
|
||||
if (result && typeof result.dispose === 'function') {
|
||||
this._register(toDisposable(result.dispose));
|
||||
}
|
||||
});
|
||||
|
||||
// Register cleanup for the DOM element
|
||||
this._register(toDisposable(() => {
|
||||
if (onboardingContainer.parentElement) {
|
||||
|
||||
@@ -99,7 +99,7 @@ const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifSe
|
||||
secondary: [{
|
||||
id: 'void.updater.close',
|
||||
enabled: true,
|
||||
label: `Keep Void outdated`,
|
||||
label: `Keep current version`,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => {
|
||||
|
||||
@@ -6,15 +6,17 @@
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
import { AnthropicReasoning, RawToolCallObj } from './sendLLMMessageTypes.js';
|
||||
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
content: string; // give this result to LLM (string of value)
|
||||
id: string;
|
||||
rawParams: RawToolParamsObj;
|
||||
} & (
|
||||
// in order of events:
|
||||
| { type: 'invalid_params', result: null, name: T, params: RawToolCallObj | null, }
|
||||
| { type: 'invalid_params', result: null, name: T, }
|
||||
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
|
||||
|
||||
@@ -22,7 +24,7 @@ export type ToolMessage<T extends ToolName> = {
|
||||
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
|
||||
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T], }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T] }
|
||||
) // user rejected
|
||||
|
||||
export type DecorativeCanceledTool = {
|
||||
@@ -58,7 +60,6 @@ export type ChatMessage =
|
||||
role: 'assistant';
|
||||
displayContent: string; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
reasoning: string; // reasoning from the LLM, used for step-by-step thinking
|
||||
toolCall: RawToolCallObj | undefined;
|
||||
|
||||
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
|
||||
const foundMid = pm.removePrefix(`<${midTag}>`)
|
||||
|
||||
if (foundMid) {
|
||||
pm.removeSuffix(`\n`) // sometimes outputs \n
|
||||
pm.removeSuffix(`</${midTag}>`)
|
||||
}
|
||||
const s = pm.value()
|
||||
|
||||
@@ -43,7 +43,22 @@ export const defaultProviderSettings = {
|
||||
},
|
||||
mistral: {
|
||||
apiKey: '',
|
||||
}
|
||||
},
|
||||
lmStudio: {
|
||||
endpoint: 'http://localhost:1234',
|
||||
},
|
||||
liteLLM: { // https://docs.litellm.ai/docs/providers/openai_compatible
|
||||
endpoint: '',
|
||||
},
|
||||
// googleVertex: { // google https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
|
||||
// region: 'us-west2',
|
||||
// project: '',
|
||||
// },
|
||||
microsoftAzure: { // microsoft Azure Foundry
|
||||
project: '', // really 'resource'
|
||||
apiKey: '',
|
||||
azureApiVersion: '2024-05-01-preview',
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -51,11 +66,15 @@ export const defaultProviderSettings = {
|
||||
|
||||
export const defaultModelsOfProvider = {
|
||||
openAI: [ // https://platform.openai.com/docs/models/gp
|
||||
'o3-mini',
|
||||
'o1',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
'gpt-4.1',
|
||||
'gpt-4.1-mini',
|
||||
'gpt-4.1-nano',
|
||||
'o3',
|
||||
'o4-mini',
|
||||
// 'o1',
|
||||
// 'o1-mini',
|
||||
// 'gpt-4o',
|
||||
// 'gpt-4o-mini',
|
||||
],
|
||||
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
'claude-3-7-sonnet-latest',
|
||||
@@ -80,6 +99,8 @@ export const defaultModelsOfProvider = {
|
||||
],
|
||||
vLLM: [ // autodetected
|
||||
],
|
||||
lmStudio: [], // autodetected
|
||||
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
// 'anthropic/claude-3.7-sonnet:thinking',
|
||||
'anthropic/claude-3.7-sonnet',
|
||||
@@ -108,6 +129,11 @@ export const defaultModelsOfProvider = {
|
||||
'ministral-8b-latest',
|
||||
],
|
||||
openAICompatible: [], // fallback
|
||||
// googleVertex: [],
|
||||
microsoftAzure: [],
|
||||
liteLLM: [],
|
||||
|
||||
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
||||
|
||||
@@ -127,6 +153,7 @@ export type VoidStaticModelInfo = { // not stateful
|
||||
}
|
||||
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special paramete
|
||||
specialToolFormat?: 'openai-style' | 'anthropic-style', // null defaults to XML
|
||||
supportsFIM: boolean;
|
||||
|
||||
reasoningCapabilities: false | {
|
||||
@@ -163,7 +190,7 @@ type VoidStaticProviderInfo = { // doesn't change (not stateful)
|
||||
|
||||
|
||||
const modelOptionsDefaults: VoidStaticModelInfo = {
|
||||
contextWindow: 32_000,
|
||||
contextWindow: 16_000,
|
||||
maxOutputTokens: 4_096,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
@@ -344,12 +371,19 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (
|
||||
|
||||
if (lower.includes('quasar') || lower.includes('quaser')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['quasar'] })
|
||||
|
||||
if (lower.includes('gpt') && lower.includes('mini') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1-mini'])
|
||||
if (lower.includes('gpt') && lower.includes('nano') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1-nano'])
|
||||
if (lower.includes('gpt') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1'])
|
||||
|
||||
if (lower.includes('4o') && lower.includes('mini')) return toFallback(openAIModelOptions['gpt-4o-mini'])
|
||||
if (lower.includes('4o')) return toFallback(openAIModelOptions['gpt-4o'])
|
||||
|
||||
if (lower.includes('o1') && lower.includes('mini')) return toFallback(openAIModelOptions['o1-mini'])
|
||||
if (lower.includes('o1')) return toFallback(openAIModelOptions['o1'])
|
||||
if (lower.includes('o3') && lower.includes('mini')) return toFallback(openAIModelOptions['o3-mini'])
|
||||
// if (lower.includes('o3')) return toFallback(openAIModelOptions['o3'])
|
||||
if (lower.includes('o3')) return toFallback(openAIModelOptions['o3'])
|
||||
if (lower.includes('o4') && lower.includes('mini')) return toFallback(openAIModelOptions['o4-mini'])
|
||||
|
||||
|
||||
if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower))
|
||||
return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat])
|
||||
@@ -370,6 +404,7 @@ const anthropicModelOptions = {
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: {
|
||||
supportsReasoning: true,
|
||||
@@ -378,6 +413,7 @@ const anthropicModelOptions = {
|
||||
reasoningMaxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
|
||||
reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
|
||||
},
|
||||
|
||||
},
|
||||
'claude-3-5-sonnet-20241022': {
|
||||
contextWindow: 200_000,
|
||||
@@ -385,6 +421,7 @@ const anthropicModelOptions = {
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
@@ -394,6 +431,7 @@ const anthropicModelOptions = {
|
||||
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
@@ -403,6 +441,7 @@ const anthropicModelOptions = {
|
||||
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
@@ -411,6 +450,7 @@ const anthropicModelOptions = {
|
||||
downloadable: false,
|
||||
maxOutputTokens: 4_096,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
}
|
||||
@@ -444,6 +484,56 @@ const anthropicSettings: VoidStaticProviderInfo = {
|
||||
|
||||
// ---------------- OPENAI ----------------
|
||||
const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
||||
'o3': {
|
||||
contextWindow: 1_047_576,
|
||||
maxOutputTokens: 32_768,
|
||||
cost: { input: 10.00, output: 40.00, cache_read: 2.50 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'o4-mini': {
|
||||
contextWindow: 1_047_576,
|
||||
maxOutputTokens: 32_768,
|
||||
cost: { input: 1.10, output: 4.40, cache_read: 0.275 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gpt-4.1': {
|
||||
contextWindow: 1_047_576,
|
||||
maxOutputTokens: 32_768,
|
||||
cost: { input: 2.00, output: 8.00, cache_read: 0.50 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gpt-4.1-mini': {
|
||||
contextWindow: 1_047_576,
|
||||
maxOutputTokens: 32_768,
|
||||
cost: { input: 0.40, output: 1.60, cache_read: 0.10 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gpt-4.1-nano': {
|
||||
contextWindow: 1_047_576,
|
||||
maxOutputTokens: 32_768,
|
||||
cost: { input: 0.10, output: 0.40, cache_read: 0.03 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'o1': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: 100_000,
|
||||
@@ -468,6 +558,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
||||
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
@@ -486,6 +577,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
||||
cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'system-role', // ??
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
@@ -516,6 +608,15 @@ const xAIModelOptions = {
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
// 'grok-3': {
|
||||
// contextWindow: 1_000_000,
|
||||
// maxOutputTokens: null,
|
||||
// cost: {},
|
||||
// downloadable: false,
|
||||
// supportsFIM: false,
|
||||
// supportsSystemMessage: 'system-role',
|
||||
// reasoningCapabilities: {canIOReasoning:false, canTurnOffReasoning:true,},
|
||||
// }
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const xAISettings: VoidStaticProviderInfo = {
|
||||
@@ -727,8 +828,27 @@ const groqSettings: VoidStaticProviderInfo = {
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
||||
|
||||
// ---------------- GOOGLE VERTEX ----------------
|
||||
// const googleVertexModelOptions = {
|
||||
// } as const satisfies Record<string, VoidStaticModelInfo>
|
||||
// const googleVertexSettings: VoidStaticProviderInfo = {
|
||||
// modelOptions: googleVertexModelOptions,
|
||||
// modelOptionsFallback: (modelName) => { return null }
|
||||
// }
|
||||
|
||||
// ---------------- MICROSOFT AZURE ----------------
|
||||
const microsoftAzureModelOptions = {
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
const microsoftAzureSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: microsoftAzureModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
||||
|
||||
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
|
||||
const ollamaModelOptions = {
|
||||
'qwen2.5-coder:3b': {
|
||||
'qwen2.5-coder:1.5b': {
|
||||
contextWindow: 32_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
@@ -737,6 +857,15 @@ const ollamaModelOptions = {
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'llama3.1': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: { sizeGb: 4.9 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwen2.5-coder': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null,
|
||||
@@ -767,12 +896,9 @@ const ollamaModelOptions = {
|
||||
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
|
||||
export const ollamaRecommendedModels = ['qwen2.5-coder:3b', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
|
||||
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
|
||||
|
||||
|
||||
|
||||
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
|
||||
|
||||
const vLLMSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
|
||||
@@ -780,6 +906,12 @@ const vLLMSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
const lmStudioSettings: VoidStaticProviderInfo = {
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
const ollamaSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: we need to filter out reasoning <think> tags manually
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
@@ -793,6 +925,12 @@ const openaiCompatible: VoidStaticProviderInfo = {
|
||||
modelOptions: {},
|
||||
}
|
||||
|
||||
const liteLLMSettings: VoidStaticProviderInfo = { // https://docs.litellm.ai/docs/reasoning_content
|
||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' } },
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
|
||||
// ---------------- OPENROUTER ----------------
|
||||
const openRouterModelOptions_assumingOpenAICompat = {
|
||||
@@ -939,9 +1077,12 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
||||
ollama: ollamaSettings,
|
||||
openAICompatible: openaiCompatible,
|
||||
mistral: mistralSettings,
|
||||
// googleVertex: {},
|
||||
// microsoftAzure: {},
|
||||
// openHands: {},
|
||||
|
||||
liteLLM: liteLLMSettings,
|
||||
lmStudio: lmStudioSettings,
|
||||
|
||||
// googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
} as const
|
||||
|
||||
|
||||
@@ -998,6 +1139,14 @@ export const getIsReasoningEnabledState = (
|
||||
}
|
||||
|
||||
|
||||
export const getMaxOutputTokens = (providerName: ProviderName, modelName: string, opts: { isReasoningEnabled: boolean }) => {
|
||||
const {
|
||||
reasoningCapabilities,
|
||||
maxOutputTokens
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
return opts.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
}
|
||||
|
||||
// used to force reasoning state (complex) into something simple we can just read from when sending a message
|
||||
export const getSendableReasoningInfo = (
|
||||
featureName: FeatureName,
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolCallObj } from '../sendLLMMessageTypes.js';
|
||||
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
|
||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||
import { approvalTypeOfToolName, ToolResultType } from '../toolsServiceTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
||||
@@ -20,6 +20,14 @@ export const MAX_DIRSTR_CHARS_TOTAL_TOOL = 20_000
|
||||
export const MAX_DIRSTR_RESULTS_TOTAL_BEGINNING = 100
|
||||
export const MAX_DIRSTR_RESULTS_TOTAL_TOOL = 100
|
||||
|
||||
// tool info
|
||||
export const MAX_FILE_CHARS_PAGE = 500_000
|
||||
export const MAX_CHILDREN_URIs_PAGE = 500
|
||||
|
||||
// terminal tool info
|
||||
export const MAX_TERMINAL_CHARS = 100_000
|
||||
export const MAX_TERMINAL_INACTIVE_TIME = 8 // seconds
|
||||
|
||||
|
||||
// Maximum character limits for prefix and suffix context
|
||||
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
|
||||
@@ -66,7 +74,36 @@ const paginationParam = {
|
||||
} as const
|
||||
|
||||
|
||||
|
||||
|
||||
// export type SnakeCase<S extends string> =
|
||||
// // exact acronym URI
|
||||
// S extends 'URI' ? 'uri'
|
||||
// // suffix URI: e.g. 'rootURI' -> snakeCase('root') + '_uri'
|
||||
// : S extends `${infer Prefix}URI` ? `${SnakeCase<Prefix>}_uri`
|
||||
// // default: for each char, prefix '_' on uppercase letters
|
||||
// : S extends `${infer C}${infer Rest}`
|
||||
// ? `${C extends Lowercase<C> ? C : `_${Lowercase<C>}`}${SnakeCase<Rest>}`
|
||||
// : S;
|
||||
|
||||
// export type SnakeCaseKeys<T extends Record<string, any>> = {
|
||||
// [K in keyof T as SnakeCase<Extract<K, string>>]: T[K]
|
||||
// };
|
||||
|
||||
|
||||
|
||||
export const voidTools = {
|
||||
// export const voidTools
|
||||
// : {
|
||||
// [T in keyof ToolCallParams]: {
|
||||
// name: string;
|
||||
// description: string;
|
||||
// params: {
|
||||
// [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// = {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
|
||||
read_file: {
|
||||
@@ -74,8 +111,8 @@ export const voidTools = {
|
||||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
start_line: { description: 'Optional. Default is 1. Start reading on this line.' },
|
||||
end_line: { description: 'Optional. Default is Infinity. Stop reading after this line.' },
|
||||
start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' },
|
||||
end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
@@ -84,13 +121,13 @@ export const voidTools = {
|
||||
name: 'ls_dir',
|
||||
description: `Lists all files and folders in the given URI.`,
|
||||
params: {
|
||||
...uriParam('folder'),
|
||||
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
get_dir_structure: {
|
||||
name: 'get_dir_structure',
|
||||
get_dir_tree: {
|
||||
name: 'get_dir_tree',
|
||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
@@ -106,24 +143,35 @@ export const voidTools = {
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
|
||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
search_files: {
|
||||
name: 'search_files',
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file contents). The query can be any substring or glob. You can follow this with read_file to view result contents.`,
|
||||
search_for_files: {
|
||||
name: 'search_for_files',
|
||||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
|
||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
// add new search_in_file tool
|
||||
search_in_file: {
|
||||
name: 'search_in_file',
|
||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
query: { description: 'The string or regex to search for in the file.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||
}
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Returns all lint errors on a given file.`,
|
||||
@@ -156,34 +204,46 @@ export const voidTools = {
|
||||
description: `Edits the contents of a file given the file's URI and a description.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
change_description: {
|
||||
change_diff: {
|
||||
description: `\
|
||||
A brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
|
||||
NEVER re-write the whole file. Instead, use comments like "// ... existing code ...". Bias towards writing as little as possible. \
|
||||
Your description will be handed to a smaller model to make the change, so it must be clear and concise. \
|
||||
Your description MUST be wrapped in triple backticks. \
|
||||
Here's an example of a good description:\n${editToolDescriptionExample}`
|
||||
A code diff describing the change to make to the file. \
|
||||
Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \
|
||||
Your DIFF MUST be wrapped in triple backticks. \
|
||||
NEVER re-write the whole file. Always bias towards writing as little as possible. \
|
||||
Use comments like "// ... existing code ..." to condense your writing. \
|
||||
Here's an example of a good output:\n${editToolDescriptionExample}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
run_terminal_command: {
|
||||
name: 'run_terminal_command',
|
||||
description: `Executes a terminal command.`,
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to execute. If working with tools like git that can paginate, you should pipe to cat so results are not truncated.' },
|
||||
wait_for_completion: { description: `Optional. Default is true. Make this value false when you want a command to run without waiting for it to complete.` },
|
||||
terminal_id: { description: 'Optional. The ID of the terminal instance that should execute the command (if not provided, defaults to the preferred terminal ID). The primary purpose of this is to let you open a new terminal for testing or background processes (e.g. running a dev server for the user in a separate terminal). Must be an integer >= 1.' },
|
||||
command: { description: 'The terminal command to run.' },
|
||||
bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' },
|
||||
},
|
||||
},
|
||||
|
||||
open_persistent_terminal: {
|
||||
name: 'open_persistent_terminal',
|
||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||
params: {}
|
||||
},
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Closes a BG terminal with the given ID.`,
|
||||
params: { terminal_id: { description: `The terminal ID to interrupt and close.` } }
|
||||
}
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
} satisfies { [name: string]: InternalToolInfo }
|
||||
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
|
||||
|
||||
|
||||
export type ToolName = keyof typeof voidTools
|
||||
export type ToolName = keyof ToolResultType
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
|
||||
@@ -198,7 +258,7 @@ export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
|
||||
export const availableTools = (chatMode: ChatMode) => {
|
||||
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))
|
||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
|
||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
||||
: undefined
|
||||
|
||||
@@ -206,7 +266,7 @@ export const availableTools = (chatMode: ChatMode) => {
|
||||
return tools
|
||||
}
|
||||
|
||||
const availableXMLToolsStr = (tools: InternalToolInfo[]) => {
|
||||
const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
|
||||
return `${tools.map((t, i) => {
|
||||
const params = Object.keys(t.params).map(paramName => `<${paramName}>${t.params[paramName].description}</${paramName}>`).join('\n')
|
||||
return `\
|
||||
@@ -218,12 +278,11 @@ Format:
|
||||
}).join('\n\n')}`
|
||||
}
|
||||
|
||||
export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
|
||||
const t = toolCall
|
||||
const params = Object.keys(t.rawParams).map(paramName => `<${paramName}>${t.rawParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
|
||||
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
|
||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
|
||||
return `\
|
||||
<${toolCall.name}>${!params ? '' : `\n${params}`}
|
||||
</${toolCall.name}>`
|
||||
<${toolName}>${!params ? '' : `\n${params}`}
|
||||
</${toolName}>`
|
||||
.replace('\t', ' ')
|
||||
}
|
||||
|
||||
@@ -231,18 +290,18 @@ export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
|
||||
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
||||
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
||||
const tools = availableTools(chatMode)
|
||||
if (!tools || tools.length === 0) return ''
|
||||
if (!tools || tools.length === 0) return null
|
||||
|
||||
const toolXMLDefinitions = (`\
|
||||
Available tools:
|
||||
|
||||
${availableXMLToolsStr(tools)}`)
|
||||
${toolCallDefinitionsXMLString(tools)}`)
|
||||
|
||||
const toolCallXMLGuidelines = (`\
|
||||
Tool calling details:
|
||||
- Once you write a tool call, you must STOP and WAIT for the result.
|
||||
- All parameters are REQUIRED unless noted otherwise.
|
||||
- To call a tool, write its name and parameters in one of the XML formats specified above.
|
||||
- After you write the tool call, you must STOP and WAIT for the result.
|
||||
- All parameters are REQUIRED unless noted otherwise.
|
||||
- You are only allowed to output ONE tool call, and it must be at the END of your response.
|
||||
- Your tool call will be executed immediately, and the results will appear in the following user message.`)
|
||||
|
||||
@@ -255,7 +314,7 @@ ${toolCallXMLGuidelines}`
|
||||
// ======================================================== chat (normal, gather, agent) ========================================================
|
||||
|
||||
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => {
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
|
||||
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
|
||||
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
|
||||
@@ -289,14 +348,14 @@ ${directoryStr}
|
||||
</files_overview>`)
|
||||
|
||||
|
||||
const toolDefinitions = systemToolsXMLPrompt(mode)
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
|
||||
|
||||
const details: string[] = []
|
||||
|
||||
if (mode === 'agent' || mode === 'gather') {
|
||||
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
|
||||
details.push('Only use ONE tool call at a time, and always wait for the result before proceeding.') // XML
|
||||
details.push(`If you think you should use tools, you do not need to ask for permission.`)
|
||||
details.push('Only use ONE tool call at a time.')
|
||||
details.push(`NEVER say something like "I'm going to use \`tool_name\`". Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc.`)
|
||||
details.push(`Many tools only work if the user has a workspace open.`)
|
||||
}
|
||||
@@ -332,6 +391,7 @@ Here's an example of a good edit suggestion:
|
||||
${fileNameEditExample}.`)
|
||||
}
|
||||
|
||||
details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`)
|
||||
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
|
||||
details.push(`Today's date is ${new Date().toDateString()}.`)
|
||||
|
||||
@@ -343,10 +403,9 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
|
||||
const ansStrs: string[] = []
|
||||
ansStrs.push(header)
|
||||
ansStrs.push(sysInfo)
|
||||
ansStrs.push(fsInfo)
|
||||
if (toolDefinitions) ansStrs.push(toolDefinitions)
|
||||
ansStrs.push(importantDetails)
|
||||
ansStrs.push('Now, please assist the user with their query.')
|
||||
ansStrs.push(fsInfo)
|
||||
|
||||
const fullSystemMsgStr = ansStrs
|
||||
.join('\n\n\n')
|
||||
@@ -449,9 +508,10 @@ export const DIVIDER = `=======`
|
||||
export const FINAL = `>>>>>>> UPDATED`
|
||||
|
||||
export const searchReplace_systemMessage = `\
|
||||
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
|
||||
You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff.
|
||||
The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`.
|
||||
|
||||
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
|
||||
Format your SEARCH/REPLACE blocks as follows:
|
||||
${tripleTick[0]}
|
||||
${ORIGINAL}
|
||||
// ... original code goes here
|
||||
@@ -460,22 +520,28 @@ ${DIVIDER}
|
||||
${FINAL}
|
||||
${tripleTick[1]}
|
||||
|
||||
You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make.
|
||||
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
|
||||
1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out.
|
||||
|
||||
Directions:
|
||||
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
|
||||
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. This includes whitespace, comments, and other details.
|
||||
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
|
||||
4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks.
|
||||
2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change.
|
||||
|
||||
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
|
||||
- Make sure you add all necessary imports.
|
||||
- Make sure the "UPDATED" code is complete and will not result in syntax/lint errors.
|
||||
3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output.
|
||||
|
||||
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
|
||||
4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
|
||||
|
||||
5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code.
|
||||
|
||||
6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible.
|
||||
|
||||
7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.
|
||||
|
||||
## EXAMPLE 1
|
||||
DIFF
|
||||
${tripleTick[0]}
|
||||
// ... existing code
|
||||
let x = 6.5
|
||||
// ... existing code
|
||||
${tripleTick[1]}
|
||||
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}
|
||||
let w = 5
|
||||
@@ -484,15 +550,6 @@ let y = 7
|
||||
let z = 8
|
||||
${tripleTick[1]}
|
||||
|
||||
CHANGE
|
||||
Make x equal to 6.5, not 6.
|
||||
${tripleTick[0]}
|
||||
// ... existing code
|
||||
let x = 6.5
|
||||
// ... existing code
|
||||
${tripleTick[1]}
|
||||
|
||||
|
||||
## ACCEPTED OUTPUT
|
||||
${tripleTick[0]}
|
||||
${ORIGINAL}
|
||||
@@ -504,11 +561,14 @@ ${tripleTick[1]}
|
||||
`
|
||||
|
||||
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
|
||||
ORIGINAL_FILE
|
||||
${originalCode}
|
||||
DIFF
|
||||
${applyStr}
|
||||
|
||||
CHANGE
|
||||
${applyStr}`
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}
|
||||
${originalCode}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
|
||||
|
||||
|
||||
@@ -630,39 +690,6 @@ ${tripleTick[1]}).`
|
||||
|
||||
|
||||
|
||||
// const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
// const { name, description, params } = toolInfo
|
||||
// return {
|
||||
// name: name,
|
||||
// description: description,
|
||||
// input_schema: {
|
||||
// type: 'object',
|
||||
// properties: params,
|
||||
// // required: Object.keys(params),
|
||||
// },
|
||||
// } satisfies Anthropic.Messages.Tool
|
||||
// }
|
||||
|
||||
|
||||
// const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||
// const { name, description, params } = toolInfo
|
||||
// return {
|
||||
// type: 'function',
|
||||
// function: {
|
||||
// name: name,
|
||||
// // strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat
|
||||
// description: description,
|
||||
// parameters: {
|
||||
// type: 'object',
|
||||
// properties: params,
|
||||
// // required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false
|
||||
// // additionalProperties: false,
|
||||
// },
|
||||
// }
|
||||
// } satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
// }
|
||||
|
||||
|
||||
/*
|
||||
// ======================================================== ai search/replace ========================================================
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ILLMMessageService } from './sendLLMMessageService.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { OllamaModelResponse, VLLMModelResponse } from './sendLLMMessageTypes.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './sendLLMMessageTypes.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
@@ -46,6 +46,7 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
||||
vLLM: ['_didFillInProviderSettings', 'endpoint'],
|
||||
lmStudio: ['_didFillInProviderSettings', 'endpoint'],
|
||||
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
@@ -142,6 +143,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
vLLM: { state: 'init', timeoutId: null },
|
||||
lmStudio: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
@@ -160,18 +162,18 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
}
|
||||
}
|
||||
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'vLLM' ? this.llmMessageService.vLLMList
|
||||
: () => { }
|
||||
: this.llmMessageService.openAICompatibleList
|
||||
|
||||
listFn({
|
||||
providerName,
|
||||
onSuccess: ({ models }) => {
|
||||
|
||||
// set the models to the detected models
|
||||
this.voidSettingsService.setAutodetectedModels(
|
||||
providerName,
|
||||
models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
||||
else if (providerName === 'vLLM') return (model as VLLMModelResponse).id;
|
||||
else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else if (providerName === 'lmStudio') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './sendLLMMessageTypes.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './sendLLMMessageTypes.js';
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
@@ -13,7 +13,6 @@ import { generateUuid } from '../../../../base/common/uuid.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');
|
||||
@@ -23,7 +22,7 @@ export interface ILLMMessageService {
|
||||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => void;
|
||||
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
||||
vLLMList: (params: ServiceModelListParams<VLLMModelResponse>) => void;
|
||||
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,12 +46,12 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) },
|
||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) },
|
||||
},
|
||||
vLLM: {
|
||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<VLLMModelResponse>) => void) },
|
||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<VLLMModelResponse>) => void) },
|
||||
openAICompat: {
|
||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) },
|
||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) },
|
||||
}
|
||||
} satisfies {
|
||||
[providerName: string]: {
|
||||
[providerName in 'ollama' | 'openAICompat']: {
|
||||
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<any>) => void) },
|
||||
error: { [eventId: string]: ((params: EventModelListOnErrorParams<any>) => void) },
|
||||
}
|
||||
@@ -71,14 +70,31 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
|
||||
// .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_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._clearChannelHooks(e.requestId) }))
|
||||
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._clearChannelHooks(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
|
||||
// ollama .list()
|
||||
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.success[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.error[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onSuccess_list_vLLM') satisfies Event<EventModelListOnSuccessParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.success[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onError_list_vLLM') satisfies Event<EventModelListOnErrorParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.error[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||
this.llmMessageHooks.onText[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||
this.llmMessageHooks.onFinalMessage[e.requestId]?.(e);
|
||||
this._clearChannelHooks(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
this.llmMessageHooks.onError[e.requestId]?.(e);
|
||||
this._clearChannelHooks(e.requestId);
|
||||
console.error('Error in LLMMessageService:', JSON.stringify(e))
|
||||
}))
|
||||
// .list()
|
||||
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||
this.listHooks.ollama.success[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.listHooks.ollama.error[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onSuccess_list_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.listHooks.openAICompat.success[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_list_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.listHooks.openAICompat.error[e.requestId]?.(e)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
@@ -98,6 +114,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
return null
|
||||
}
|
||||
|
||||
const { settingsOfProvider, } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId = generateUuid();
|
||||
@@ -106,13 +123,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
this.llmMessageHooks.onError[requestId] = onError
|
||||
this.llmMessageHooks.onAbort[requestId] = onAbort // used internally only
|
||||
|
||||
const { aiInstructions } = this.voidSettingsService.state.globalSettings
|
||||
const { settingsOfProvider, } = this.voidSettingsService.state
|
||||
|
||||
// params will be stripped of all its functions over the IPC channel
|
||||
this.channel.call('sendLLMMessage', {
|
||||
...proxyParams,
|
||||
aiInstructions,
|
||||
requestId,
|
||||
settingsOfProvider,
|
||||
modelSelection,
|
||||
@@ -147,25 +160,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
}
|
||||
|
||||
|
||||
vLLMList = (params: ServiceModelListParams<VLLMModelResponse>) => {
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.listHooks.vLLM.success[requestId_] = onSuccess
|
||||
this.listHooks.vLLM.error[requestId_] = onError
|
||||
this.listHooks.openAICompat.success[requestId_] = onSuccess
|
||||
this.listHooks.openAICompat.error[requestId_] = onError
|
||||
|
||||
this.channel.call('vLLMList', {
|
||||
this.channel.call('openAICompatibleList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
providerName: 'vLLM',
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<VLLMModelResponse>)
|
||||
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||
}
|
||||
|
||||
_clearChannelHooks(requestId: string) {
|
||||
private _clearChannelHooks(requestId: string) {
|
||||
delete this.llmMessageHooks.onText[requestId]
|
||||
delete this.llmMessageHooks.onFinalMessage[requestId]
|
||||
delete this.llmMessageHooks.onError[requestId]
|
||||
@@ -173,8 +185,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
delete this.listHooks.ollama.success[requestId]
|
||||
delete this.listHooks.ollama.error[requestId]
|
||||
|
||||
delete this.listHooks.vLLM.success[requestId]
|
||||
delete this.listHooks.vLLM.error[requestId]
|
||||
delete this.listHooks.openAICompat.success[requestId]
|
||||
delete this.listHooks.openAICompat.error[requestId]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolName, ToolParamName } from './prompt/prompts.js'
|
||||
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export const errorDetails = (fullError: Error | null): string | null => {
|
||||
@@ -27,16 +27,39 @@ export const getErrorMessage: (error: unknown) => string = (error) => {
|
||||
}
|
||||
|
||||
|
||||
export type LLMChatMessage = {
|
||||
role: 'system';
|
||||
content: string;
|
||||
|
||||
export type AnthropicLLMChatMessage = {
|
||||
role: 'assistant',
|
||||
content: string | (AnthropicReasoning | { type: 'text'; text: string }
|
||||
| { type: 'tool_use'; name: string; input: Record<string, any>; id: string; }
|
||||
)[];
|
||||
} | {
|
||||
role: 'user';
|
||||
role: 'user',
|
||||
content: string | (
|
||||
{ type: 'text'; text: string; } | { type: 'tool_result'; tool_use_id: string; content: string; }
|
||||
)[]
|
||||
}
|
||||
export type OpenAILLMChatMessage = {
|
||||
role: 'system' | 'user' | 'developer';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant',
|
||||
content: string; // text content
|
||||
anthropicReasoning: AnthropicReasoning[] | null;
|
||||
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
|
||||
tool_calls?: { type: 'function'; id: string; function: { name: string; arguments: string; } }[];
|
||||
} | {
|
||||
role: 'tool',
|
||||
content: string;
|
||||
tool_call_id: string;
|
||||
}
|
||||
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
|
||||
|
||||
|
||||
|
||||
|
||||
export type LLMFIMMessage = {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
stopTokens: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +70,10 @@ export type RawToolCallObj = {
|
||||
name: ToolName;
|
||||
rawParams: RawToolParamsObj;
|
||||
doneParams: ToolParamName[];
|
||||
id: string;
|
||||
isDone: boolean;
|
||||
};
|
||||
|
||||
|
||||
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
|
||||
|
||||
export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj }) => void
|
||||
@@ -60,23 +83,18 @@ export type OnAbort = () => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
|
||||
export type LLMFIMMessage = {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
stopTokens: string[];
|
||||
}
|
||||
|
||||
// service types
|
||||
type SendLLMType = {
|
||||
messagesType: 'chatMessages';
|
||||
messages: LLMChatMessage[];
|
||||
messages: LLMChatMessage[]; // the type of raw chat messages that we send to Anthropic, OAI, etc
|
||||
separateSystemMessage: string | undefined;
|
||||
chatMode: ChatMode | null;
|
||||
} | {
|
||||
messagesType: 'FIMMessage';
|
||||
messages: LLMFIMMessage;
|
||||
separateSystemMessage?: undefined;
|
||||
chatMode?: undefined;
|
||||
}
|
||||
|
||||
// service types
|
||||
export type ServiceSendLLMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
@@ -95,8 +113,6 @@ export type SendLLMMessageParams = {
|
||||
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
|
||||
abortRef: AbortRef;
|
||||
|
||||
aiInstructions: string;
|
||||
|
||||
modelSelection: ModelSelection;
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined;
|
||||
|
||||
@@ -146,15 +162,13 @@ export type OllamaModelResponse = {
|
||||
size_vram: number;
|
||||
}
|
||||
|
||||
type OpenaiCompatibleModelResponse = {
|
||||
export type OpenaiCompatibleModelResponse = {
|
||||
id: string;
|
||||
created: number;
|
||||
object: 'model';
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
export type VLLMModelResponse = OpenaiCompatibleModelResponse
|
||||
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
@@ -167,12 +181,13 @@ export type ModelListParams<ModelResponse> = {
|
||||
|
||||
// params to the service
|
||||
export type ServiceModelListParams<modelResponse> = {
|
||||
providerName: RefreshableProviderName;
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onError: (param: { error: any }) => void;
|
||||
}
|
||||
|
||||
type BlockedMainModelListParams = 'onSuccess' | 'onError'
|
||||
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
|
||||
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { providerName: RefreshableProviderName, requestId: string }
|
||||
|
||||
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
|
||||
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ToolName } from './prompt/prompts.js';
|
||||
|
||||
|
||||
|
||||
export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
|
||||
export type TerminalResolveReason = { type: 'timeout' } | { type: 'done', exitCode: number }
|
||||
|
||||
export type LintErrorItem = { code: string, message: string, startLineNumber: number, endLineNumber: number }
|
||||
|
||||
@@ -16,39 +16,57 @@ export type ShallowDirectoryItem = {
|
||||
}
|
||||
|
||||
|
||||
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
|
||||
'create_file_or_folder': 'edits',
|
||||
'delete_file_or_folder': 'edits',
|
||||
'edit_file': 'edits',
|
||||
'run_command': 'terminal',
|
||||
}
|
||||
|
||||
const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'run_terminal_command'] as const satisfies readonly ToolName[]
|
||||
export type ToolNameWithApproval = typeof toolNamesWithApproval[number]
|
||||
export const toolNamesThatRequireApproval = new Set<ToolName>(toolNamesWithApproval)
|
||||
|
||||
|
||||
// {{add: define new type for approval types}}
|
||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>;
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>(
|
||||
Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined)
|
||||
)
|
||||
|
||||
// PARAMS OF TOOL CALL
|
||||
export type ToolCallParams = {
|
||||
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
||||
'ls_dir': { rootURI: URI, pageNumber: number },
|
||||
'get_dir_structure': { rootURI: URI },
|
||||
'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number },
|
||||
'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
|
||||
'ls_dir': { uri: URI, pageNumber: number },
|
||||
'get_dir_tree': { uri: URI },
|
||||
'search_pathnames_only': { query: string, includePattern: string | null, pageNumber: number },
|
||||
'search_for_files': { query: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
|
||||
'search_in_file': { uri: URI, query: string, isRegex: boolean },
|
||||
'read_lint_errors': { uri: URI },
|
||||
// ---
|
||||
'edit_file': { uri: URI, changeDescription: string },
|
||||
'edit_file': { uri: URI, changeDiff: string },
|
||||
'create_file_or_folder': { uri: URI, isFolder: boolean },
|
||||
'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean },
|
||||
'run_terminal_command': { command: string, proposedTerminalId: string, waitForCompletion: boolean },
|
||||
// ---
|
||||
'run_command': { command: string; bgTerminalId: string | null },
|
||||
'open_persistent_terminal': {},
|
||||
'kill_persistent_terminal': { terminalId: string },
|
||||
}
|
||||
|
||||
|
||||
// RESULT OF TOOL CALL
|
||||
export type ToolResultType = {
|
||||
'read_file': { fileContents: string, hasNextPage: boolean },
|
||||
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
|
||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'get_dir_structure': { str: string, },
|
||||
'get_dir_tree': { str: string, },
|
||||
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
|
||||
'search_files': { uris: URI[], hasNextPage: boolean },
|
||||
'search_for_files': { uris: URI[], hasNextPage: boolean },
|
||||
'search_in_file': { lines: number[]; },
|
||||
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
|
||||
// ---
|
||||
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
|
||||
'create_file_or_folder': {},
|
||||
'delete_file_or_folder': {},
|
||||
'run_terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; },
|
||||
// ---
|
||||
'run_command': { result: string; resolveReason: TerminalResolveReason; },
|
||||
'open_persistent_terminal': { terminalId: string },
|
||||
'kill_persistent_terminal': {},
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,15 @@ class VoidModelService extends Disposable implements IVoidModelService {
|
||||
}
|
||||
|
||||
initializeModel = async (uri: URI) => {
|
||||
if (uri.fsPath in this._modelRefOfURI) return;
|
||||
const editorModelRef = await this._textModelService.createModelReference(uri);
|
||||
// Keep a strong reference to prevent disposal
|
||||
this._modelRefOfURI[uri.fsPath] = editorModelRef;
|
||||
try {
|
||||
if (uri.fsPath in this._modelRefOfURI) return;
|
||||
const editorModelRef = await this._textModelService.createModelReference(uri);
|
||||
// Keep a strong reference to prevent disposal
|
||||
this._modelRefOfURI[uri.fsPath] = editorModelRef;
|
||||
}
|
||||
catch (e) {
|
||||
console.log('InitializeModel error:', e)
|
||||
}
|
||||
};
|
||||
|
||||
getModelFromFsPath = (fsPath: string): VoidModelType => {
|
||||
|
||||
@@ -62,6 +62,9 @@ export interface IVoidSettingsService {
|
||||
setOptionsOfModelSelection: SetOptionsOfModelSelection;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
|
||||
dangerousSetState(newState: VoidSettingsState): Promise<void>;
|
||||
resetState(): Promise<void>;
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
@@ -71,24 +74,22 @@ export interface IVoidSettingsService {
|
||||
|
||||
|
||||
|
||||
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[] }) => {
|
||||
const { existingModels } = options
|
||||
const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulModelInfo[], models: string[], type: 'autodetected' | 'default' }) => {
|
||||
const { existingModels, models, type } = options
|
||||
|
||||
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
const newDefaultModels = defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: true,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
}))
|
||||
const newDefaultModels = models.map((modelName, i) => ({ modelName, type, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
|
||||
|
||||
return [
|
||||
...newDefaultModels, // swap out all the default models for the new default models
|
||||
...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models
|
||||
...newDefaultModels, // swap out all the models of this type for the new models of this type
|
||||
...existingModels.filter(m => {
|
||||
const keep = m.type !== type
|
||||
return keep
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
@@ -101,7 +102,30 @@ export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter:
|
||||
}
|
||||
|
||||
|
||||
const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
|
||||
const _stateWithMergedDefaultModels = (state: VoidSettingsState): VoidSettingsState => {
|
||||
let newSettingsOfProvider = state.settingsOfProvider
|
||||
|
||||
// recompute default models
|
||||
for (const providerName of providerNames) {
|
||||
const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? []
|
||||
const currentModels = newSettingsOfProvider[providerName]?.models ?? []
|
||||
const defaultModelNames = defaultModels.map(m => m.modelName)
|
||||
const newModels = _modelsWithSwappedInNewModels({ existingModels: currentModels, models: defaultModelNames, type: 'default' })
|
||||
newSettingsOfProvider = {
|
||||
...newSettingsOfProvider,
|
||||
[providerName]: {
|
||||
...newSettingsOfProvider[providerName],
|
||||
models: newModels,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
}
|
||||
}
|
||||
|
||||
const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>): VoidSettingsState => {
|
||||
|
||||
let newSettingsOfProvider = state.settingsOfProvider
|
||||
|
||||
@@ -210,23 +234,76 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
this.readAndInitializeState()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
dangerousSetState = async (newState: VoidSettingsState) => {
|
||||
this.state = _validatedModelState(newState)
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onUpdate_syncApplyToChat()
|
||||
}
|
||||
async resetState() {
|
||||
await this.dangerousSetState(defaultState())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async readAndInitializeState() {
|
||||
let readS: VoidSettingsState
|
||||
try {
|
||||
readS = await this._readState();
|
||||
// 1.0.3 addition, remove when enough users have had this code run
|
||||
if (readS.globalSettings.includeToolLintErrors === undefined) readS.globalSettings.includeToolLintErrors = true
|
||||
|
||||
// autoapprove is now an obj not a boolean (1.2.5)
|
||||
if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {}
|
||||
}
|
||||
catch (e) {
|
||||
readS = defaultState()
|
||||
}
|
||||
|
||||
// the stored data structure might be outdated, so we need to update it here
|
||||
const finalState = readS
|
||||
this.state = _validatedModelState(finalState);
|
||||
try {
|
||||
readS = {
|
||||
...readS,
|
||||
...defaultSettingsOfProvider,
|
||||
...readS.settingsOfProvider,
|
||||
}
|
||||
|
||||
for (const providerName of providerNames) {
|
||||
readS.settingsOfProvider[providerName] = {
|
||||
...defaultSettingsOfProvider[providerName],
|
||||
...readS.settingsOfProvider[providerName],
|
||||
} as any
|
||||
|
||||
// conversion from 1.0.3 to 1.2.5 (can remove this when enough people update)
|
||||
for (const m of readS.settingsOfProvider[providerName].models) {
|
||||
if (!m.type) {
|
||||
const old = (m as { isAutodetected?: boolean; isDefault?: boolean })
|
||||
if (old.isAutodetected)
|
||||
m.type = 'autodetected'
|
||||
else if (old.isDefault)
|
||||
m.type = 'default'
|
||||
else m.type = 'custom'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catch (e) {
|
||||
readS = defaultState()
|
||||
}
|
||||
|
||||
this.state = readS
|
||||
this.state = _stateWithMergedDefaultModels(this.state)
|
||||
this.state = _validatedModelState(this.state);
|
||||
|
||||
|
||||
this._resolver();
|
||||
this._onDidChangeState.fire();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -353,7 +430,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const oldModelNames = models.map(m => m.modelName)
|
||||
|
||||
const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models })
|
||||
const newModels = _modelsWithSwappedInNewModels({ existingModels: models, models: autodetectedModelNames, type: 'autodetected' })
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
// if the models changed, log it
|
||||
@@ -387,7 +464,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
{ modelName, type: 'custom', isHidden: false } as const
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
|
||||
import { ToolApprovalType } from './toolsServiceTypes.js';
|
||||
import { VoidSettingsState } from './voidSettingsService.js'
|
||||
|
||||
|
||||
@@ -15,7 +16,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
export const localProviderNames = ['ollama', 'vLLM'] satisfies ProviderName[] // all local names
|
||||
export const localProviderNames = ['ollama', 'vLLM', 'lmStudio'] satisfies ProviderName[] // all local names
|
||||
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
@@ -30,9 +31,8 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
||||
|
||||
export type VoidStatefulModelInfo = { // <-- STATEFUL
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
type: 'default' | 'autodetected' | 'custom';
|
||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
||||
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
|
||||
|
||||
|
||||
@@ -59,69 +59,78 @@ type DisplayInfoForProviderName = {
|
||||
|
||||
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
|
||||
if (providerName === 'anthropic') {
|
||||
return {
|
||||
title: 'Anthropic',
|
||||
}
|
||||
return { title: 'Anthropic', }
|
||||
}
|
||||
else if (providerName === 'openAI') {
|
||||
return {
|
||||
title: 'OpenAI',
|
||||
}
|
||||
return { title: 'OpenAI', }
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
return {
|
||||
title: 'DeepSeek.com API',
|
||||
}
|
||||
return { title: 'DeepSeek', }
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
return {
|
||||
title: 'OpenRouter',
|
||||
}
|
||||
return { title: 'OpenRouter', }
|
||||
}
|
||||
else if (providerName === 'ollama') {
|
||||
return {
|
||||
title: 'Ollama',
|
||||
}
|
||||
return { title: 'Ollama', }
|
||||
}
|
||||
else if (providerName === 'vLLM') {
|
||||
return {
|
||||
title: 'vLLM',
|
||||
}
|
||||
return { title: 'vLLM', }
|
||||
}
|
||||
else if (providerName === 'liteLLM') {
|
||||
return { title: 'LiteLLM', }
|
||||
}
|
||||
else if (providerName === 'lmStudio') {
|
||||
return { title: 'LM Studio', }
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
return {
|
||||
title: 'OpenAI-Compatible',
|
||||
}
|
||||
return { title: 'OpenAI-Compatible', }
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
return {
|
||||
title: 'Gemini API',
|
||||
}
|
||||
return { title: 'Gemini', }
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
return {
|
||||
title: 'Groq.com API',
|
||||
}
|
||||
return { title: 'Groq', }
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
return {
|
||||
title: 'Grok (xAI)',
|
||||
}
|
||||
return { title: 'xAI', }
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return {
|
||||
title: 'Mistral API',
|
||||
}
|
||||
return { title: 'Mistral', }
|
||||
}
|
||||
// else if (providerName === 'googleVertex') {
|
||||
// return { title: 'Google Vertex AI', }
|
||||
// }
|
||||
else if (providerName === 'microsoftAzure') {
|
||||
return { title: 'Microsoft Azure OpenAI', }
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
||||
export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
||||
|
||||
if (providerName === 'anthropic') return 'Get your [API Key here](https://console.anthropic.com/settings/keys).'
|
||||
if (providerName === 'openAI') return 'Get your [API Key here](https://platform.openai.com/api-keys).'
|
||||
if (providerName === 'deepseek') return 'Get your [API Key here](https://platform.deepseek.com/api_keys).'
|
||||
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys).'
|
||||
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey).'
|
||||
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
|
||||
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
|
||||
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
|
||||
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
|
||||
// if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
|
||||
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
|
||||
if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
|
||||
if (providerName === 'vLLM') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
|
||||
if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
|
||||
|
||||
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
||||
type DisplayInfo = {
|
||||
title: string;
|
||||
placeholder: string;
|
||||
subTextMd?: string;
|
||||
isPasswordField?: boolean;
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
@@ -135,23 +144,15 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'openAI' ? 'sk-proj-key...' :
|
||||
providerName === 'deepseek' ? 'sk-key...' :
|
||||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||
providerName === 'gemini' ? 'key...' :
|
||||
providerName === 'gemini' ? 'AIzaSy...' :
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
providerName === 'xAI' ? 'xai-key...' :
|
||||
providerName === 'mistral' ? 'api-key...' :
|
||||
'',
|
||||
// providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
'',
|
||||
|
||||
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
|
||||
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
|
||||
providerName === 'deepseek' ? 'Get your [API Key here](https://platform.deepseek.com/api_keys).' :
|
||||
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
|
||||
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
|
||||
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
|
||||
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
|
||||
providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys).' :
|
||||
providerName === 'openAICompatible' ? undefined :
|
||||
'',
|
||||
isPasswordField: true,
|
||||
}
|
||||
}
|
||||
@@ -159,19 +160,51 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
'(never)',
|
||||
providerName === 'lmStudio' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
// providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
|
||||
: providerName === 'liteLLM' ? 'http://localhost:4000'
|
||||
: '(never)',
|
||||
|
||||
|
||||
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
|
||||
providerName === 'vLLM' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' :
|
||||
undefined,
|
||||
}
|
||||
}
|
||||
// else if (settingName === 'region') {
|
||||
// // vertex only
|
||||
// return {
|
||||
// title: 'Region',
|
||||
// placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
|
||||
// : ''
|
||||
// }
|
||||
// }
|
||||
else if (settingName === 'azureApiVersion') {
|
||||
// azure only
|
||||
return {
|
||||
title: 'API Version',
|
||||
placeholder: providerName === 'microsoftAzure' ? defaultProviderSettings.microsoftAzure.azureApiVersion
|
||||
: ''
|
||||
}
|
||||
}
|
||||
else if (settingName === 'project') {
|
||||
return {
|
||||
title: providerName === 'microsoftAzure' ? 'Resource'
|
||||
// : providerName === 'googleVertex' ? 'Project'
|
||||
: '',
|
||||
placeholder: providerName === 'microsoftAzure' ? 'my-resource'
|
||||
// : providerName === 'googleVertex' ? 'my-project'
|
||||
: ''
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
else if (settingName === '_didFillInProviderSettings') {
|
||||
return {
|
||||
title: '(never)',
|
||||
@@ -195,6 +228,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
// region: undefined, // googleVertex
|
||||
project: undefined,
|
||||
azureApiVersion: undefined,
|
||||
}
|
||||
|
||||
|
||||
@@ -202,8 +238,7 @@ const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: Vo
|
||||
return {
|
||||
models: defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: false,
|
||||
type: 'default',
|
||||
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
|
||||
}))
|
||||
}
|
||||
@@ -247,6 +282,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
liteLLM: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.liteLLM,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.liteLLM),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
lmStudio: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.lmStudio,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.lmStudio),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
groq: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
@@ -277,6 +324,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
// googleVertex: { // aggregator (serves models from multiple providers)
|
||||
// ...defaultCustomSettings,
|
||||
// ...defaultProviderSettings.googleVertex,
|
||||
// ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
|
||||
// _didFillInProviderSettings: undefined,
|
||||
// },
|
||||
microsoftAzure: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.microsoftAzure,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -367,7 +426,7 @@ export type GlobalSettings = {
|
||||
syncApplyToChat: boolean;
|
||||
enableFastApply: boolean;
|
||||
chatMode: ChatMode;
|
||||
autoApprove: boolean;
|
||||
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
|
||||
showInlineSuggestions: boolean;
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
@@ -380,10 +439,10 @@ export const defaultGlobalSettings: GlobalSettings = {
|
||||
syncApplyToChat: true,
|
||||
enableFastApply: true,
|
||||
chatMode: 'agent',
|
||||
autoApprove: false,
|
||||
autoApprove: {},
|
||||
showInlineSuggestions: true,
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: true,
|
||||
isOnboardingComplete: false,
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js'
|
||||
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
|
||||
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
|
||||
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
|
||||
@@ -134,7 +135,7 @@ export const extractReasoningWrapper = (
|
||||
}
|
||||
|
||||
|
||||
// =============== tools ===============
|
||||
// =============== tools (XML) ===============
|
||||
|
||||
|
||||
|
||||
@@ -160,7 +161,7 @@ const findIndexOfAny = (fullText: string, matches: string[]) => {
|
||||
|
||||
|
||||
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
|
||||
const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const paramsObj: RawToolParamsObj = {}
|
||||
const doneParams: ToolParamName[] = []
|
||||
let isDone = false
|
||||
@@ -179,7 +180,8 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolNam
|
||||
name: toolName,
|
||||
rawParams: paramsObj,
|
||||
doneParams: doneParams,
|
||||
isDone: isDone
|
||||
isDone: isDone,
|
||||
id: toolId,
|
||||
}
|
||||
return ans
|
||||
}
|
||||
@@ -254,9 +256,11 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolNam
|
||||
}
|
||||
}
|
||||
|
||||
export const extractToolsWrapper = (
|
||||
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode
|
||||
export const extractXMLToolsWrapper = (
|
||||
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
|
||||
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
|
||||
|
||||
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
const tools = availableTools(chatMode)
|
||||
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
|
||||
@@ -264,6 +268,8 @@ export const extractToolsWrapper = (
|
||||
const toolOpenTags = tools.map(t => `<${t.name}>`)
|
||||
for (const t of tools) { toolOfToolName[t.name] = t }
|
||||
|
||||
const toolId = generateUuid()
|
||||
|
||||
// detect <availableTools[0]></availableTools[0]>, etc
|
||||
let fullText = '';
|
||||
let trueFullText = ''
|
||||
@@ -315,14 +321,12 @@ export const extractToolsWrapper = (
|
||||
if (foundOpenTag !== null) {
|
||||
latestToolCall = parseXMLPrefixToToolCall(
|
||||
foundOpenTag.toolName,
|
||||
toolId,
|
||||
trueFullText.substring(foundOpenTag.idx, Infinity),
|
||||
toolOfToolName,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
onText({
|
||||
...params,
|
||||
fullText,
|
||||
|
||||
@@ -1,524 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AnthropicReasoning, LLMChatMessage, LLMFIMMessage } from '../../common/sendLLMMessageTypes.js';
|
||||
import { deepClone } from '../../../../../base/common/objects.js';
|
||||
|
||||
|
||||
export const parseObject = (args: unknown) => {
|
||||
if (typeof args === 'object')
|
||||
return args
|
||||
if (typeof args === 'string')
|
||||
try { return JSON.parse(args) }
|
||||
catch (e) { return { args } }
|
||||
return {}
|
||||
}
|
||||
|
||||
|
||||
type InternalLLMChatMessage = {
|
||||
role: 'system' | 'user';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant',
|
||||
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
|
||||
}
|
||||
|
||||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }): { messages: LLMChatMessage[] } => {
|
||||
const messages = deepClone(messages_)
|
||||
const newMessages: LLMChatMessage[] = []
|
||||
if (messages.length >= 0) newMessages.push(messages[0])
|
||||
|
||||
// remove duplicate roles - we used to do this, but we don't anymore
|
||||
for (let i = 1; i < messages.length; i += 1) {
|
||||
const m = messages[i]
|
||||
newMessages.push(m)
|
||||
}
|
||||
const finalMessages = newMessages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
return { messages: finalMessages }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const TRIM_TO_LEN = 60
|
||||
|
||||
const prepareMessages_fitIntoContext = ({ messages, contextWindow, maxOutputTokens }: { messages: LLMChatMessage[], contextWindow: number, maxOutputTokens: number }): { messages: LLMChatMessage[] } => {
|
||||
|
||||
// the higher the weight, the higher the desire to truncate
|
||||
const alreadyTrimmedIdxes = new Set<number>()
|
||||
const weight = (message: LLMChatMessage, messages: LLMChatMessage[], idx: number) => {
|
||||
const base = message.content.length
|
||||
|
||||
let multiplier: number
|
||||
if (message.role === 'system')
|
||||
return 0 // never erase system message
|
||||
|
||||
multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases
|
||||
if (message.role === 'user') {
|
||||
multiplier *= 1
|
||||
}
|
||||
else {
|
||||
multiplier *= 10 // llm tokens are far less valuable than user tokens
|
||||
}
|
||||
|
||||
// 1st message, last 3 msgs, any already modified message should be low in weight
|
||||
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
|
||||
multiplier *= .05
|
||||
}
|
||||
|
||||
return base * multiplier
|
||||
|
||||
}
|
||||
const _findLargestByWeight = (messages: LLMChatMessage[]) => {
|
||||
let largestIndex = -1
|
||||
let largestWeight = -Infinity
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const m = messages[i]
|
||||
const w = weight(m, messages, i)
|
||||
if (w > largestWeight) {
|
||||
largestWeight = w
|
||||
largestIndex = i
|
||||
}
|
||||
}
|
||||
return largestIndex
|
||||
}
|
||||
|
||||
let totalLen = 0
|
||||
for (const m of messages) { totalLen += m.content.length }
|
||||
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
|
||||
if (charsNeedToTrim <= 0) return { messages }
|
||||
|
||||
// <----------------------------------------->
|
||||
// 0 | | |
|
||||
// | contextWindow |
|
||||
// contextWindow - maxOut|putTokens
|
||||
// |
|
||||
// totalLen
|
||||
|
||||
|
||||
// TRIM HIGHEST WEIGHT MESSAGES
|
||||
let remainingCharsToTrim = charsNeedToTrim
|
||||
let i = 0
|
||||
|
||||
while (remainingCharsToTrim > 0) {
|
||||
i += 1
|
||||
if (i > 100) break
|
||||
|
||||
const trimIdx = _findLargestByWeight(messages)
|
||||
const m = messages[trimIdx]
|
||||
|
||||
// if can finish here, do
|
||||
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
|
||||
if (numCharsWillTrim > remainingCharsToTrim) {
|
||||
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim)
|
||||
break
|
||||
}
|
||||
|
||||
remainingCharsToTrim -= numCharsWillTrim
|
||||
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
|
||||
alreadyTrimmedIdxes.add(trimIdx)
|
||||
}
|
||||
|
||||
return { messages }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
|
||||
const prepareMessages_addSystemInstructions = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
}: {
|
||||
messages: InternalLLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
})
|
||||
: { separateSystemMessageStr?: string, messages: any[] } => {
|
||||
|
||||
// find system messages and concatenate them
|
||||
let systemMessageStr = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n') || undefined;
|
||||
|
||||
if (aiInstructions)
|
||||
systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}`
|
||||
|
||||
let separateSystemMessageStr: string | undefined = undefined
|
||||
|
||||
// remove all system messages
|
||||
const newMessages: (InternalLLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system')
|
||||
|
||||
|
||||
// if it has a system message (if doesn't, we obviously don't care about whether it supports system message or not...)
|
||||
if (systemMessageStr) {
|
||||
// if supports system message
|
||||
if (supportsSystemMessage) {
|
||||
if (supportsSystemMessage === 'separated')
|
||||
separateSystemMessageStr = systemMessageStr
|
||||
else if (supportsSystemMessage === 'system-role')
|
||||
newMessages.unshift({ role: 'system', content: systemMessageStr }) // add new first message
|
||||
else if (supportsSystemMessage === 'developer-role')
|
||||
newMessages.unshift({ role: 'developer', content: systemMessageStr }) // add new first message
|
||||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
const newFirstMessage = {
|
||||
role: 'user',
|
||||
content: (''
|
||||
+ '<SYSTEM_MESSAGE>\n'
|
||||
+ systemMessageStr
|
||||
+ '\n'
|
||||
+ '</SYSTEM_MESSAGE>\n'
|
||||
+ newMessages[0].content
|
||||
)
|
||||
} as const
|
||||
newMessages.splice(0, 1) // delete first message
|
||||
newMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
}
|
||||
|
||||
return { messages: newMessages, separateSystemMessageStr }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // convert messages as if about to send to openai
|
||||
// /*
|
||||
// reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
|
||||
// openai MESSAGE (role=assistant):
|
||||
// "tool_calls":[{
|
||||
// "type": "function",
|
||||
// "id": "call_12345xyz",
|
||||
// "function": {
|
||||
// "name": "get_weather",
|
||||
// "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
|
||||
// }]
|
||||
|
||||
// openai RESPONSE (role=user):
|
||||
// { "role": "tool",
|
||||
// "tool_call_id": tool_call.id,
|
||||
// "content": str(result) }
|
||||
|
||||
// also see
|
||||
// openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
// openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
// */
|
||||
|
||||
// type PrepareMessagesToolsOpenAI = (
|
||||
// Exclude<InternalLLMChatMessage, { role: 'assistant' | 'tool' }> | {
|
||||
// role: 'assistant',
|
||||
// content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
|
||||
// tool_calls?: {
|
||||
// type: 'function';
|
||||
// id: string;
|
||||
// function: {
|
||||
// name: string;
|
||||
// arguments: string;
|
||||
// }
|
||||
// }[]
|
||||
// } | {
|
||||
// role: 'tool',
|
||||
// tool_call_id: string;
|
||||
// content: string;
|
||||
// }
|
||||
// )[]
|
||||
// const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
|
||||
|
||||
// const newMessages: PrepareMessagesToolsOpenAI = [];
|
||||
|
||||
// for (let i = 0; i < messages.length; i += 1) {
|
||||
// const currMsg = messages[i]
|
||||
|
||||
// if (currMsg.role !== 'tool') {
|
||||
// newMessages.push(currMsg)
|
||||
// continue
|
||||
// }
|
||||
|
||||
// // edit previous assistant message to have called the tool
|
||||
// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
// if (prevMsg?.role === 'assistant') {
|
||||
// prevMsg.tool_calls = [{
|
||||
// type: 'function',
|
||||
// id: currMsg.id,
|
||||
// function: {
|
||||
// name: currMsg.name,
|
||||
// arguments: JSON.stringify(currMsg.params)
|
||||
// }
|
||||
// }]
|
||||
// }
|
||||
|
||||
// // add the tool
|
||||
// newMessages.push({
|
||||
// role: 'tool',
|
||||
// tool_call_id: currMsg.id,
|
||||
// content: currMsg.content || EMPTY_TOOL_CONTENT,
|
||||
// })
|
||||
// }
|
||||
// return { messages: newMessages }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// // convert messages as if about to send to anthropic
|
||||
// /*
|
||||
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
|
||||
// anthropic MESSAGE (role=assistant):
|
||||
// "content": [{
|
||||
// "type": "text",
|
||||
// "text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
|
||||
// }, {
|
||||
// "type": "tool_use",
|
||||
// "id": "toolu_01A09q90qw90lq917835lq9",
|
||||
// "name": "get_weather",
|
||||
// "input": { "location": "San Francisco, CA", "unit": "celsius" }
|
||||
// }]
|
||||
// anthropic RESPONSE (role=user):
|
||||
// "content": [{
|
||||
// "type": "tool_result",
|
||||
// "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
|
||||
// "content": "15 degrees"
|
||||
// }]
|
||||
// */
|
||||
|
||||
// type PrepareMessagesToolsAnthropic = (
|
||||
// Exclude<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
|
||||
// role: 'assistant',
|
||||
// content: string | (
|
||||
// | AnthropicReasoning
|
||||
// | {
|
||||
// type: 'text';
|
||||
// text: string;
|
||||
// }
|
||||
// | {
|
||||
// type: 'tool_use';
|
||||
// name: string;
|
||||
// input: Record<string, any>;
|
||||
// id: string;
|
||||
// })[]
|
||||
// } | {
|
||||
// role: 'user',
|
||||
// content: string | ({
|
||||
// type: 'text';
|
||||
// text: string;
|
||||
// } | {
|
||||
// type: 'tool_result';
|
||||
// tool_use_id: string;
|
||||
// content: string;
|
||||
// })[]
|
||||
// }
|
||||
// )[]
|
||||
// /*
|
||||
// Converts:
|
||||
|
||||
// assistant: ...content
|
||||
// tool: (id, name, params)
|
||||
// ->
|
||||
// assistant: ...content, call(name, id, params)
|
||||
// user: ...content, result(id, content)
|
||||
// */
|
||||
// const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
|
||||
// const newMessages: PrepareMessagesToolsAnthropic = messages;
|
||||
|
||||
|
||||
// for (let i = 0; i < newMessages.length; i += 1) {
|
||||
// const currMsg = newMessages[i]
|
||||
|
||||
// if (currMsg.role !== 'tool') continue
|
||||
|
||||
// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
|
||||
// if (prevMsg?.role === 'assistant') {
|
||||
// if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
|
||||
// prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
|
||||
// }
|
||||
|
||||
// // turn each tool into a user message with tool results at the end
|
||||
// newMessages[i] = {
|
||||
// role: 'user',
|
||||
// content: [
|
||||
// ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const,
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// return { messages: newMessages }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI
|
||||
|
||||
// const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style' }): { messages: PrepareMessagesTools } => {
|
||||
// if (!supportsTools) {
|
||||
// return { messages: messages }
|
||||
// }
|
||||
// else if (supportsTools === 'anthropic-style') {
|
||||
// return prepareMessages_tools_anthropic({ messages })
|
||||
// }
|
||||
// else if (supportsTools === 'openai-style') {
|
||||
// return prepareMessages_tools_openai({ messages })
|
||||
// }
|
||||
// else {
|
||||
// throw new Error(`supportsTools type not recognized`)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent
|
||||
const prepareMessages_anthropicReasoning = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => {
|
||||
const newMessages: InternalLLMChatMessage[] = []
|
||||
for (const m of messages) {
|
||||
if (m.role !== 'assistant') {
|
||||
newMessages.push(m)
|
||||
continue
|
||||
}
|
||||
let newMessage: InternalLLMChatMessage
|
||||
if (supportsAnthropicReasoningSignature && m.anthropicReasoning) {
|
||||
const content = m.content ? [...m.anthropicReasoning, { type: 'text' as const, text: m.content }] : m.anthropicReasoning
|
||||
newMessage = { role: 'assistant', content: content }
|
||||
}
|
||||
else {
|
||||
newMessage = { role: 'assistant', content: m.content }
|
||||
}
|
||||
newMessages.push(newMessage)
|
||||
}
|
||||
return { messages: newMessages }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// do this at end
|
||||
const prepareMessages_noEmptyMessage = ({ messages }: { messages: InternalLLMChatMessage[] }): { messages: InternalLLMChatMessage[] } => {
|
||||
for (const currMsg of messages) {
|
||||
// if content is a string, replace string with empty msg
|
||||
if (typeof currMsg.content === 'string')
|
||||
currMsg.content = currMsg.content || EMPTY_MESSAGE
|
||||
else {
|
||||
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
|
||||
for (const c of currMsg.content) {
|
||||
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
|
||||
}
|
||||
if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
|
||||
}
|
||||
}
|
||||
return { messages }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- CHAT ---
|
||||
|
||||
export const prepareMessages = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
supportsAnthropicReasoningSignature,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
}: {
|
||||
messages: LLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
supportsAnthropicReasoningSignature: boolean,
|
||||
contextWindow: number,
|
||||
maxOutputTokens: number | null | undefined,
|
||||
}) => {
|
||||
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
|
||||
|
||||
const { messages: messages0 } = prepareMessages_normalize({ messages })
|
||||
const { messages: messages1 } = prepareMessages_fitIntoContext({ messages: messages0, contextWindow, maxOutputTokens })
|
||||
const { messages: messages2 } = prepareMessages_anthropicReasoning({ messages: messages1, supportsAnthropicReasoningSignature })
|
||||
const { messages: messages3, separateSystemMessageStr } = prepareMessages_addSystemInstructions({ messages: messages2, aiInstructions, supportsSystemMessage })
|
||||
const { messages: messages4 } = prepareMessages_noEmptyMessage({ messages: messages3 })
|
||||
|
||||
return {
|
||||
messages: messages4 as any,
|
||||
separateSystemMessageStr
|
||||
} as const
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// --- FIM ---
|
||||
|
||||
export const prepareFIMMessage = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
}: {
|
||||
messages: LLMFIMMessage,
|
||||
aiInstructions: string,
|
||||
}) => {
|
||||
|
||||
let prefix = `\
|
||||
${!aiInstructions ? '' : `\
|
||||
// Instructions:
|
||||
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
|
||||
${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
|
||||
|
||||
${messages.prefix}`
|
||||
|
||||
const suffix = messages.suffix
|
||||
const stopTokens = messages.stopTokens
|
||||
const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Gemini has this, but they're openai-compat so we don't need to implement this
|
||||
gemini request:
|
||||
{ "role": "assistant",
|
||||
"content": null,
|
||||
"function_call": {
|
||||
"name": "get_weather",
|
||||
"arguments": {
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gemini response:
|
||||
{ "role": "assistant",
|
||||
"function_response": {
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"temperature": "15°C",
|
||||
"condition": "Cloudy"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,22 +3,26 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// disable foreign import complaints
|
||||
/* eslint-disable */
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { Ollama } from 'ollama';
|
||||
import OpenAI, { ClientOptions } from 'openai';
|
||||
import { MistralCore } from '@mistralai/mistralai/core.js';
|
||||
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
|
||||
// import { GoogleAuth } from 'google-auth-library'
|
||||
/* eslint-enable */
|
||||
|
||||
|
||||
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js';
|
||||
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
|
||||
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings } from '../../common/modelCapabilities.js';
|
||||
import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js';
|
||||
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
|
||||
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
|
||||
|
||||
|
||||
|
||||
|
||||
type InternalCommonMessageParams = {
|
||||
aiInstructions: string;
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
@@ -29,8 +33,8 @@ type InternalCommonMessageParams = {
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}
|
||||
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; }
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; }
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
|
||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||
|
||||
|
||||
@@ -38,7 +42,16 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
||||
|
||||
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
// const getGoogleApiKey = async () => {
|
||||
// // module‑level singleton
|
||||
// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
|
||||
// const key = await auth.getAccessToken()
|
||||
// if (!key) throw new Error(`Google API failed to generate a key.`)
|
||||
// return key
|
||||
// }
|
||||
|
||||
|
||||
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
const commonPayloadOpts: ClientOptions = {
|
||||
dangerouslyAllowBrowser: true,
|
||||
...includeInPayload,
|
||||
@@ -55,6 +68,14 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'liteLLM') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'lmStudio') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
@@ -69,8 +90,21 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
// else if (providerName === 'googleVertex') {
|
||||
// // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
|
||||
// const thisConfig = settingsOfProvider[providerName]
|
||||
// const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
|
||||
// return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
|
||||
// }
|
||||
else if (providerName === 'microsoftAzure') {
|
||||
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions??api-version=${thisConfig.azureApiVersion}`
|
||||
return new OpenAI({ baseURL: baseURL, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
@@ -96,7 +130,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
||||
}
|
||||
|
||||
|
||||
const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => {
|
||||
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
|
||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
@@ -106,16 +140,14 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
|
||||
return
|
||||
}
|
||||
|
||||
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
|
||||
|
||||
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
openai.completions
|
||||
.create({
|
||||
model: modelName,
|
||||
prompt: messages.prefix,
|
||||
suffix: messages.suffix,
|
||||
stop: messages.stopTokens,
|
||||
max_tokens: messages.maxTokens,
|
||||
prompt: prefix,
|
||||
suffix: suffix,
|
||||
stop: stopTokens,
|
||||
max_tokens: 300,
|
||||
})
|
||||
.then(async response => {
|
||||
const fullText = response.choices[0]?.text
|
||||
@@ -128,14 +160,61 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
|
||||
}
|
||||
|
||||
|
||||
const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params } = toolInfo
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: name,
|
||||
// strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat
|
||||
description: description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
// required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false
|
||||
// additionalProperties: false,
|
||||
},
|
||||
}
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
const openAITools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
|
||||
for (const t in allowedTools ?? {}) {
|
||||
openAITools.push(toOpenAICompatibleTool(allowedTools[t]))
|
||||
}
|
||||
return openAITools
|
||||
}
|
||||
|
||||
const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
let input: unknown
|
||||
try {
|
||||
input = JSON.parse(toolParamsStr)
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
}
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, chatMode }: SendChatParams_Internal) => {
|
||||
// ------------ OPENAI-COMPATIBLE ------------
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
supportsSystemMessage,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
specialToolFormat,
|
||||
reasoningCapabilities,
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
|
||||
@@ -146,16 +225,19 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
// max tokens
|
||||
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
|
||||
{ tools: potentialTools } as const
|
||||
: {}
|
||||
|
||||
// instance
|
||||
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens })
|
||||
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
const openai: OpenAI = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
messages: messages as any,
|
||||
stream: true,
|
||||
...nativeToolsObj,
|
||||
// max_completion_tokens: maxTokens,
|
||||
}
|
||||
|
||||
@@ -168,9 +250,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
|
||||
// manually parse out tool results
|
||||
if (chatMode) {
|
||||
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
@@ -178,6 +260,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
||||
let fullReasoningSoFar = ''
|
||||
let fullTextSoFar = ''
|
||||
|
||||
let toolName = ''
|
||||
let toolId = ''
|
||||
let toolParamsStr = ''
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
@@ -188,6 +274,17 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
||||
const newText = chunk.choices[0]?.delta?.content ?? ''
|
||||
fullTextSoFar += newText
|
||||
|
||||
// tool call
|
||||
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
|
||||
const index = tool.index
|
||||
if (index !== 0) continue
|
||||
|
||||
toolName += tool.function?.name ?? ''
|
||||
toolParamsStr += tool.function?.arguments ?? '';
|
||||
toolId += tool.id ?? ''
|
||||
}
|
||||
|
||||
|
||||
// reasoning
|
||||
let newReasoning = ''
|
||||
if (nameOfReasoningFieldInDelta) {
|
||||
@@ -196,14 +293,21 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
||||
fullReasoningSoFar += newReasoning
|
||||
}
|
||||
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
|
||||
})
|
||||
|
||||
}
|
||||
// on final
|
||||
if (!fullTextSoFar && !fullReasoningSoFar) {
|
||||
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
}
|
||||
else {
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null });
|
||||
const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId)
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||
}
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
@@ -229,7 +333,7 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
|
||||
onError_({ error })
|
||||
}
|
||||
try {
|
||||
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: OpenAIModel[] = []
|
||||
@@ -251,14 +355,48 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
|
||||
|
||||
|
||||
|
||||
// ------------ ANTHROPIC (HELPERS) ------------
|
||||
const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params } = toolInfo
|
||||
return {
|
||||
name: name,
|
||||
description: description,
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
// required: Object.keys(params),
|
||||
},
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
const anthropicTools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
|
||||
for (const t in allowedTools ?? {}) {
|
||||
anthropicTools.push(toAnthropicTool(allowedTools[t]))
|
||||
}
|
||||
return anthropicTools
|
||||
}
|
||||
|
||||
const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
||||
const { id, name, input } = toolBlock
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
}
|
||||
|
||||
// ------------ ANTHROPIC ------------
|
||||
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, chatMode }: SendChatParams_Internal) => {
|
||||
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
supportsSystemMessage,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
reasoningCapabilities,
|
||||
specialToolFormat,
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
@@ -269,26 +407,34 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
// anthropic-specific - max tokens
|
||||
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled })
|
||||
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ?
|
||||
{ tools: potentialTools, tool_choice: { type: 'auto' } } as const
|
||||
: {}
|
||||
|
||||
|
||||
// instance
|
||||
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: thisConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: separateSystemMessageStr,
|
||||
messages: messages,
|
||||
system: separateSystemMessage ?? undefined,
|
||||
messages: messages as AnthropicLLMChatMessage[],
|
||||
model: modelName,
|
||||
max_tokens: maxTokens ?? 4_096, // anthropic requires this
|
||||
...includeInPayload,
|
||||
...nativeToolsObj,
|
||||
|
||||
})
|
||||
|
||||
// manually parse out tool results
|
||||
if (chatMode) {
|
||||
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
@@ -297,9 +443,17 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
||||
let fullText = ''
|
||||
let fullReasoning = ''
|
||||
|
||||
// let fullToolName = ''
|
||||
// let fullToolParams = ''
|
||||
let fullToolName = ''
|
||||
let fullToolParams = ''
|
||||
|
||||
|
||||
const runOnText = () => {
|
||||
onText({
|
||||
fullText,
|
||||
fullReasoning,
|
||||
toolCall: isAToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
|
||||
})
|
||||
}
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
stream.on('streamEvent', e => {
|
||||
// start block
|
||||
@@ -307,46 +461,49 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
||||
if (e.content_block.type === 'text') {
|
||||
if (fullText) fullText += '\n\n' // starting a 2nd text block
|
||||
fullText += e.content_block.text
|
||||
onText({ fullText, fullReasoning, })
|
||||
runOnText()
|
||||
}
|
||||
else if (e.content_block.type === 'thinking') {
|
||||
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
|
||||
fullReasoning += e.content_block.thinking
|
||||
onText({ fullText, fullReasoning, })
|
||||
runOnText()
|
||||
}
|
||||
else if (e.content_block.type === 'redacted_thinking') {
|
||||
console.log('delta', e.content_block.type)
|
||||
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
|
||||
fullReasoning += '[redacted_thinking]'
|
||||
onText({ fullText, fullReasoning, })
|
||||
runOnText()
|
||||
}
|
||||
else if (e.content_block.type === 'tool_use') {
|
||||
fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
|
||||
runOnText()
|
||||
}
|
||||
// else if (e.content_block.type === 'tool_use') {
|
||||
// fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
|
||||
// onText({ fullText, fullReasoning, })
|
||||
// }
|
||||
}
|
||||
|
||||
// delta
|
||||
else if (e.type === 'content_block_delta') {
|
||||
if (e.delta.type === 'text_delta') {
|
||||
fullText += e.delta.text
|
||||
onText({ fullText, fullReasoning, })
|
||||
runOnText()
|
||||
}
|
||||
else if (e.delta.type === 'thinking_delta') {
|
||||
fullReasoning += e.delta.thinking
|
||||
onText({ fullText, fullReasoning, })
|
||||
runOnText()
|
||||
}
|
||||
else if (e.delta.type === 'input_json_delta') { // tool use
|
||||
fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
|
||||
runOnText()
|
||||
}
|
||||
// else if (e.delta.type === 'input_json_delta') { // tool use
|
||||
// fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
|
||||
// onText({ fullText, fullReasoning, })
|
||||
// }
|
||||
}
|
||||
})
|
||||
|
||||
// on done - (or when error/fail) - this is called AFTER last streamEvent
|
||||
stream.on('finalMessage', (response) => {
|
||||
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning })
|
||||
const tools = response.content.filter(c => c.type === 'tool_use')
|
||||
const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0])
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
||||
})
|
||||
// on error
|
||||
stream.on('error', (error) => {
|
||||
@@ -360,7 +517,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
||||
|
||||
// ------------ MISTRAL ------------
|
||||
// https://docs.mistral.ai/api/#tag/fim
|
||||
const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendFIMParams_Internal) => {
|
||||
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
|
||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
@@ -369,7 +526,6 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings
|
||||
onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null })
|
||||
return
|
||||
}
|
||||
const messages = prepareFIMMessage({ messages: messages_, aiInstructions })
|
||||
|
||||
const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey })
|
||||
fimComplete(mistral,
|
||||
@@ -378,10 +534,11 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings
|
||||
prompt: messages.prefix,
|
||||
suffix: messages.suffix,
|
||||
stream: false,
|
||||
maxTokens: messages.maxTokens,
|
||||
maxTokens: 300,
|
||||
stop: messages.stopTokens,
|
||||
})
|
||||
.then(async response => {
|
||||
// unfortunately, _setAborter() does not exist
|
||||
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
|
||||
const fullText = typeof content === 'string' ? content
|
||||
: content.map(chunk => (chunk.type === 'text' ? chunk.text : '')).join('')
|
||||
@@ -426,12 +583,10 @@ const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOf
|
||||
}
|
||||
}
|
||||
|
||||
const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName, aiInstructions, _setAborter }: SendFIMParams_Internal) => {
|
||||
const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
|
||||
|
||||
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
|
||||
|
||||
let fullText = ''
|
||||
ollama.generate({
|
||||
model: modelName,
|
||||
@@ -439,7 +594,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO
|
||||
suffix: messages.suffix,
|
||||
options: {
|
||||
stop: messages.stopTokens,
|
||||
num_predict: messages.maxTokens, // max tokens
|
||||
num_predict: 300, // max tokens
|
||||
// repeat_penalty: 1,
|
||||
},
|
||||
raw: true,
|
||||
@@ -463,7 +618,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO
|
||||
|
||||
type CallFnOfProvider = {
|
||||
[providerName in ProviderName]: {
|
||||
sendChat: (params: SendChatParams_Internal) => void;
|
||||
sendChat: (params: SendChatParams_Internal) => Promise<void>;
|
||||
sendFIM: ((params: SendFIMParams_Internal) => void) | null;
|
||||
list: ((params: ListParams_Internal<any>) => void) | null;
|
||||
}
|
||||
@@ -525,6 +680,27 @@ export const sendLLMMessageToProviderImplementation = {
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
|
||||
lmStudio: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null, // lmStudio has no suffix parameter in /completions
|
||||
list: (params) => _openaiCompatibleList(params),
|
||||
},
|
||||
liteLLM: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
// googleVertex: {
|
||||
// sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
// sendFIM: null,
|
||||
// list: null,
|
||||
// },
|
||||
microsoftAzure: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
} satisfies CallFnOfProvider
|
||||
|
||||
|
||||
|
||||
@@ -9,9 +9,8 @@ import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js';
|
||||
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
export const sendLLMMessage = async ({
|
||||
messagesType,
|
||||
aiInstructions,
|
||||
messages: messages_,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
@@ -22,6 +21,7 @@ export const sendLLMMessage = ({
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
chatMode,
|
||||
separateSystemMessage,
|
||||
}: SendLLMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
@@ -108,12 +108,12 @@ export const sendLLMMessage = ({
|
||||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, chatMode })
|
||||
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
if (sendFIM) {
|
||||
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions })
|
||||
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
|
||||
return
|
||||
}
|
||||
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/sendLLMMessageTypes.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, MainModelListParams, } from '../common/sendLLMMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './llmMessage/sendLLMMessage.impl.js';
|
||||
@@ -25,7 +25,7 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
}
|
||||
|
||||
// aborters for above
|
||||
private readonly abortRefOfRequestId: Record<string, AbortRef> = {}
|
||||
private readonly _infoOfRunningRequest: Record<string, { waitForSend: Promise<void> | undefined, abortRef: AbortRef }> = {}
|
||||
|
||||
|
||||
// list
|
||||
@@ -34,12 +34,12 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
success: new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>(),
|
||||
},
|
||||
vLLM: {
|
||||
success: new Emitter<EventModelListOnSuccessParams<VLLMModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<VLLMModelResponse>>(),
|
||||
}
|
||||
openaiCompat: {
|
||||
success: new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>(),
|
||||
},
|
||||
} satisfies {
|
||||
[providerName: string]: {
|
||||
[providerName in 'ollama' | 'openaiCompat']: {
|
||||
success: Emitter<EventModelListOnSuccessParams<any>>,
|
||||
error: Emitter<EventModelListOnErrorParams<any>>,
|
||||
}
|
||||
@@ -59,8 +59,8 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
// list
|
||||
else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event;
|
||||
else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event;
|
||||
else if (event === 'onSuccess_list_vLLM') return this.listEmitters.vLLM.success.event;
|
||||
else if (event === 'onError_list_vLLM') return this.listEmitters.vLLM.error.event;
|
||||
else if (event === 'onSuccess_list_openAICompatible') return this.listEmitters.openaiCompat.success.event;
|
||||
else if (event === 'onError_list_openAICompatible') return this.listEmitters.openaiCompat.error.event;
|
||||
|
||||
else throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
@@ -72,13 +72,13 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
this._callSendLLMMessage(params)
|
||||
}
|
||||
else if (command === 'abort') {
|
||||
this._callAbort(params)
|
||||
await this._callAbort(params)
|
||||
}
|
||||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else if (command === 'vLLMList') {
|
||||
this._callVLLMList(params)
|
||||
else if (command === 'openAICompatibleList') {
|
||||
this._callOpenAICompatibleList(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
@@ -90,27 +90,37 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
}
|
||||
|
||||
// the only place sendLLMMessage is actually called
|
||||
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
||||
private _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this.abortRefOfRequestId))
|
||||
this.abortRefOfRequestId[requestId] = { current: null }
|
||||
if (!(requestId in this._infoOfRunningRequest))
|
||||
this._infoOfRunningRequest[requestId] = { waitForSend: undefined, abortRef: { current: null } }
|
||||
|
||||
const mainThreadParams: SendLLMMessageParams = {
|
||||
...params,
|
||||
onText: (p) => { this.llmMessageEmitters.onText.fire({ requestId, ...p }); },
|
||||
onFinalMessage: (p) => { this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p }); },
|
||||
onError: (p) => { console.log('sendLLM: firing err'); this.llmMessageEmitters.onError.fire({ requestId, ...p }); },
|
||||
abortRef: this.abortRefOfRequestId[requestId],
|
||||
onText: (p) => {
|
||||
this.llmMessageEmitters.onText.fire({ requestId, ...p });
|
||||
},
|
||||
onFinalMessage: (p) => {
|
||||
this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p });
|
||||
},
|
||||
onError: (p) => {
|
||||
console.log('sendLLM: firing err');
|
||||
this.llmMessageEmitters.onError.fire({ requestId, ...p });
|
||||
},
|
||||
abortRef: this._infoOfRunningRequest[requestId].abortRef,
|
||||
}
|
||||
sendLLMMessage(mainThreadParams, this.metricsService);
|
||||
const p = sendLLMMessage(mainThreadParams, this.metricsService);
|
||||
this._infoOfRunningRequest[requestId].waitForSend = p
|
||||
}
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
private async _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this.abortRefOfRequestId)) return
|
||||
this.abortRefOfRequestId[requestId].current?.()
|
||||
delete this.abortRefOfRequestId[requestId]
|
||||
if (!(requestId in this._infoOfRunningRequest)) return
|
||||
const { waitForSend, abortRef } = this._infoOfRunningRequest[requestId]
|
||||
await waitForSend // wait for the send to finish so we know abortRef was set
|
||||
abortRef?.current?.()
|
||||
delete this._infoOfRunningRequest[requestId]
|
||||
}
|
||||
|
||||
|
||||
@@ -128,15 +138,15 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams)
|
||||
}
|
||||
|
||||
_callVLLMList = (params: MainModelListParams<VLLMModelResponse>) => {
|
||||
const { requestId } = params
|
||||
const emitters = this.listEmitters.vLLM
|
||||
const mainThreadParams: ModelListParams<VLLMModelResponse> = {
|
||||
_callOpenAICompatibleList = (params: MainModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { requestId, providerName } = params
|
||||
const emitters = this.listEmitters.openaiCompat
|
||||
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||
...params,
|
||||
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
|
||||
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
|
||||
}
|
||||
sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams)
|
||||
sendLLMMessageToProviderImplementation[providerName].list(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
||||
super()
|
||||
}
|
||||
|
||||
|
||||
async check(explicit: boolean): Promise<VoidCheckUpdateRespose> {
|
||||
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
@@ -37,17 +38,17 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
||||
|
||||
if (this._updateService.state.type === StateType.Uninitialized) {
|
||||
// The update service hasn't been initialized yet
|
||||
return { message: explicit ? 'Not yet checking for updates...' : null, action: explicit ? 'reinstall' : undefined } as const
|
||||
return { message: explicit ? 'Checking for updates soon...' : null, action: explicit ? 'reinstall' : undefined } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.Idle) {
|
||||
// No updates currently available
|
||||
return { message: explicit ? 'No update found!' : null, action: explicit ? 'reinstall' : undefined } as const
|
||||
return { message: explicit ? 'No updates found!' : null, action: explicit ? 'reinstall' : undefined } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.CheckingForUpdates) {
|
||||
// Currently checking for updates
|
||||
return { message: explicit ? 'Currently checking for updates...' : null } as const
|
||||
return { message: explicit ? 'Checking for updates...' : null } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.AvailableForDownload) {
|
||||
@@ -62,7 +63,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
||||
|
||||
if (this._updateService.state.type === StateType.Downloaded) {
|
||||
// Update has been downloaded but not yet ready
|
||||
return { message: explicit ? 'Got download, need to apply...' : null, action: 'apply' } as const
|
||||
return { message: explicit ? 'An update is ready to be applied!' : null, action: 'apply' } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.Updating) {
|
||||
@@ -76,28 +77,69 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.Disabled) {
|
||||
try {
|
||||
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
|
||||
const resJSON = await res.json()
|
||||
return await this._manualCheckGHTagIfDisabled(explicit)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (!resJSON) return null // null means error
|
||||
|
||||
const { hasUpdate, downloadMessage } = resJSON ?? {}
|
||||
if (hasUpdate === undefined)
|
||||
return null
|
||||
|
||||
const after = (downloadMessage || '') + ''
|
||||
if (hasUpdate)
|
||||
return { message: after, action: 'reinstall' } as const
|
||||
return { message: 'Void is up-to-date!' } as const
|
||||
|
||||
|
||||
|
||||
private async _manualCheckGHTagIfDisabled(explicit: boolean): Promise<VoidCheckUpdateRespose> {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/voideditor/binaries/releases/latest');
|
||||
|
||||
const data = await response.json();
|
||||
const version = data.tag_name;
|
||||
|
||||
const myVersion = `${this._productService.voidVersion}.${this._productService.release}`
|
||||
const latestVersion = version
|
||||
|
||||
const isUpToDate = myVersion === latestVersion // only makes sense if response.ok
|
||||
|
||||
let message: string | null
|
||||
let action: 'reinstall' | undefined
|
||||
|
||||
// explicit
|
||||
if (explicit) {
|
||||
if (response.ok) {
|
||||
if (!isUpToDate) {
|
||||
message = 'A new version of Void is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
|
||||
action = 'reinstall'
|
||||
}
|
||||
else {
|
||||
message = 'Void is up-to-date!'
|
||||
}
|
||||
}
|
||||
else {
|
||||
message = `An error occurred when fetching the latest GitHub release tag. Please try again in ~5 minutes, or reinstall.`
|
||||
action = 'reinstall'
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
// not explicit
|
||||
else {
|
||||
if (response.ok && !isUpToDate) {
|
||||
message = 'A new version of Void is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
|
||||
action = 'reinstall'
|
||||
}
|
||||
else {
|
||||
message = null
|
||||
}
|
||||
}
|
||||
return { message, action } as const
|
||||
}
|
||||
catch (e) {
|
||||
if (explicit) {
|
||||
return {
|
||||
message: `An error occurred when fetching the latest GitHub release tag: ${e}. Please try again in ~5 minutes.`,
|
||||
action: 'reinstall',
|
||||
}
|
||||
}
|
||||
else {
|
||||
return { message: null } as const
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user