Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 6990216fbe | |||
| f5d68d9348 | |||
| 165c5aac24 | |||
| 22f4a75c00 | |||
| 06513168c1 | |||
| a80883c10f | |||
| 20b0fd6af9 | |||
| 5171f7244f | |||
| 43708d25e7 | |||
| 2b54733242 | |||
| cd77d1a1c4 | |||
| 37af09add8 | |||
| ade1e70536 | |||
| 159e0b677f | |||
| 335ebe1e11 | |||
| 7be1ae5003 | |||
| 5d7f8fc91f | |||
| d6b3bdee35 | |||
| 8c1626f79b | |||
| 97ffe0b4cc | |||
| 9cf316f808 | |||
| aabacba2a9 | |||
| 39ccb1bbbd | |||
| 63560308b5 | |||
| 967aff2d19 | |||
| 50327a238e | |||
| 8eb5abe429 | |||
| 3c532478de | |||
| 5d5839736e | |||
| b8fd40e735 | |||
| a869ddb6f3 | |||
| e5a40e3d35 | |||
| 1081b8879f | |||
| 4efc5209ab | |||
| adcdeed094 | |||
| 86285e2393 | |||
| c1b6b2e01d | |||
| 2dcdd78316 | |||
| fa9a3db86f | |||
| 4445ff28ce | |||
| 9d6a4c4648 | |||
| e41f8790b5 | |||
| d363edb1a8 | |||
| 3cddc5d5cc | |||
| 2509290ab2 | |||
| aba49edfdd | |||
| 13b60f7ee3 | |||
| bd5cf464c2 | |||
| 44e7d10ece | |||
| 6bf79b27c0 | |||
| bb22aefb6a | |||
| 7002f320d6 | |||
| def8803919 | |||
| fdb21a68b8 | |||
| 455acc3490 | |||
| bc5b07b26f | |||
| 79ad57b112 | |||
| 9391dcb0b8 | |||
| b90716e38e | |||
| d8d21b0f25 | |||
| 3bf06bfe26 | |||
| 9166fa8684 | |||
| df2165403e | |||
| 44e0f0dfc7 | |||
| d6e496c396 | |||
| 7c94b95df4 | |||
| 85759eeb4a | |||
| 6230971dff | |||
| 8f08e40819 | |||
| ae635f0102 | |||
| cb8cc587f6 | |||
| 77f931c923 | |||
| 8dcb5d35df | |||
| 6483dcffa8 | |||
| ccaf6e2e3d | |||
| d5ae1a328c | |||
| 45c83ee661 | |||
| af7dbb4b70 | |||
| 72a6daaa58 | |||
| 898a5803dd | |||
| cffc6f6db9 | |||
| b2ba634cc4 | |||
| c62d6a6790 | |||
| f8bee1d4a2 | |||
| 41e66a73c4 | |||
| 5da0e4821f | |||
| 6aefdc3095 | |||
| 4055dc7775 | |||
| f48889627a | |||
| 3c59428fe8 | |||
| 8a8a982d21 | |||
| 7f890b946d | |||
| 1799ae9d84 | |||
| c7bdd2d64c | |||
| 5fde9194af | |||
| b6408512d3 | |||
| 5e83d419ce | |||
| b1626849ba | |||
| 2f078326c2 | |||
| 448be9a2cb | |||
| 0a1f675f45 | |||
| 795ce62ce8 | |||
| 2d9fe3b0be | |||
| 03992c1206 | |||
| 0c30c2681d | |||
| 4f183f6772 | |||
| f61a7c91e4 | |||
| 610009eb1c | |||
| cd1b783917 | |||
| 015a7c10dc | |||
| f0c8eb55a6 | |||
| e0aee9863e | |||
| 4cc7f292b0 | |||
| 7c991b4dfc | |||
| 84d17737cf | |||
| 4c6b32633b | |||
| b933c5cf45 | |||
| 5b76b4a6a3 | |||
| 251d175517 | |||
| 69ce84c8df | |||
| f9e9771a87 | |||
| 18ff819ae9 | |||
| 98c09c653a | |||
| e1d4d85846 | |||
| e52e55a431 | |||
| 32d465dbe8 | |||
| 1f9bc92b66 | |||
| 9e5c82d6f8 | |||
| dd429208ce | |||
| 82278ab115 | |||
| 13cb8d4de7 | |||
| 2bc004f54d | |||
| e58b6041b4 | |||
| f7d4eebf56 | |||
| 8e735ec453 | |||
| 850bca80ab | |||
| 440dd809e3 | |||
| ffb416e4e8 | |||
| 0a17a9de19 | |||
| a0a7900381 | |||
| 5f77b9faae | |||
| 3fc97c1a83 | |||
| a3a8a7a842 | |||
| e28449b83d | |||
| 6ecc5cdbad | |||
| d64b6fba31 | |||
| d7ac86a6bd | |||
| 598f0d47f8 | |||
| 3b1ccdd2b1 | |||
| 054a8ad85a | |||
| 31ae3ea46e | |||
| 67d453cffe | |||
| ab557d4707 | |||
| e5a21bf7c6 | |||
| d5ef3b7d9b | |||
| f3753c8da0 |
@@ -12,11 +12,9 @@ There are a few ways to contribute:
|
||||
|
||||
### Codebase Guide
|
||||
|
||||
We [highly recommend reading this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization too. Void's codebase is pretty simple when you know what a service is and what `browser/` and `common/` mean, and the article covers all the jargon.
|
||||
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute!
|
||||
|
||||
<!-- ADD BLOG HERE
|
||||
We wrote a [guide to working in VSCode].
|
||||
-->
|
||||
The repo is not as intimidating as it first seems if you read the guide!
|
||||
|
||||
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
@@ -15,25 +15,26 @@ This repo contains the full sourcecode for Void. We are currently in [open beta]
|
||||
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
|
||||
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
|
||||
- 📝 [Changelog](https://voideditor.com/changelog)
|
||||
|
||||
- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
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/CONTRIBUTING.md).
|
||||
2. To get started working on Void, see [here](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 some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
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).
|
||||
|
||||
## Support
|
||||
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Void Codebase Guide
|
||||
|
||||
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).
|
||||
|
||||
|
||||
|
||||
## Void Codebase Guide
|
||||
|
||||
### Terminology
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
See [here](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for a decent VSCode guide with even more info.
|
||||
|
||||
|
||||
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
|
||||
|
||||
Here's a picture of all the dependencies that are relevent between the time you first send a message through Void's sidebar, and the time a request is sent to your provider.
|
||||
Sending LLM messages from the main process avoids CSP issues with local providers and lets us use node_modules more easily.
|
||||
|
||||
|
||||
<div align="center">
|
||||
<img width="100%" src="https://github.com/user-attachments/assets/9cf54dbb-82c4-4488-97a2-bd8dea890b50">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
**Notes:** `modelCapabilities` is an important file that must be updated when new models come out!
|
||||
|
||||
|
||||
### Apply
|
||||
|
||||
Void has two types of Apply: **Fast Apply** (uses Search/Replace, see below), and **Slow Apply** (rewrites whole file).
|
||||
|
||||
When you click Apply and Fast Apply is enabled, we prompt the LLM to output Search/Replace block(s) like this:
|
||||
```
|
||||
<<<<<<< ORIGINAL
|
||||
// original code goes here
|
||||
=======
|
||||
// replaced code goes here
|
||||
>>>>>>> UPDATED
|
||||
```
|
||||
This is what allows Void to quickly apply code even on 1000-line files. It's the same as asking the LLM to press Ctrl+F and enter in a search/replace query.
|
||||
|
||||
### Apply Inner Workings
|
||||
|
||||
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.
|
||||
- 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`.
|
||||
|
||||
### Void Settings Inner Workings
|
||||
We have a service `voidSettingsService` that stores all your Void settings (providers, models, global Void settings, etc). Imagine this as an implicit dependency for any of the core Void services:
|
||||
|
||||
<div align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/9f3cb68c-a61b-4810-8429-bb90b992b3fa">
|
||||
</div>
|
||||
|
||||
Here's a guide to some of the terminology we're using:
|
||||
- **FeatureName**: Autocomplete | Chat | CtrlK | Apply
|
||||
- **ModelSelection**: a {providerName, modelName} pair.
|
||||
- **ProviderName**: The name of a provider: `'ollama'`, `'openAI'`, etc.
|
||||
- **ModelName**: The name of a model (string type, eg `'gpt-4o'`).
|
||||
- **RefreshProvider**: a provider that we ping repeatedly to update the models list.
|
||||
- **ChatMode** = normal | gather | agent
|
||||
|
||||
|
||||
|
||||
### Approval State
|
||||
`editCodeService`'s data structures contain all the information about changes that the user needs to review. However, they don't store that information in a useful format. We wrote the following service to get a more useful derived state:
|
||||
|
||||
<div align="center">
|
||||
<img width="600" src="https://github.com/user-attachments/assets/f3645355-dff6-467c-bc38-ffe52077c08b">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
### Build process
|
||||
If you want to know how our build pipeline works, see our build repo [here](https://github.com/voideditor/void-builder).
|
||||
|
||||
|
||||
|
||||
## VSCode Codebase Guide (Not Void)
|
||||
|
||||
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!
|
||||
|
||||
#### Links for Beginners
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
#### Links for Contributors
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
|
||||
#### Misc
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
- Note: VSCode's repo is the source code for the Monaco editor! An "editor" is a Monaco editor, and it shares the code for ITextModel, etc.
|
||||
|
||||
|
||||
#### VSCode's Extension API
|
||||
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
|
||||
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
|
||||
|
||||
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
|
||||
|
||||
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
|
||||
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Useful links
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
|
||||
|
||||
For a complete guide on building Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
## Beginners / Getting started
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
|
||||
## Misc
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
|
||||
## VSCode's Extension API
|
||||
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
|
||||
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
|
||||
|
||||
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
|
||||
|
||||
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
|
||||
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.0.3",
|
||||
"voidVersion": "1.2.1",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
|
||||
@@ -52,7 +52,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
|
||||
static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState';
|
||||
|
||||
// Use the side bar dimensions
|
||||
override readonly minimumWidth: number = 230; // Void changed this (was 170)
|
||||
override readonly minimumWidth: number = 280; // Void changed this (was 170)
|
||||
override readonly maximumWidth: number = Number.POSITIVE_INFINITY;
|
||||
override readonly minimumHeight: number = 0;
|
||||
override readonly maximumHeight: number = Number.POSITIVE_INFINITY;
|
||||
|
||||
@@ -654,10 +654,12 @@ const defaultChat = {
|
||||
submenu: MenuId.ChatTitleBarMenu,
|
||||
title: localize('title4', "Copilot"),
|
||||
icon: Codicon.copilot,
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.supported,
|
||||
ContextKeyExpr.has('config.chat.commandCenter.enabled')
|
||||
),
|
||||
// Void commented this out - copilot head
|
||||
when: ContextKeyExpr.false(),
|
||||
// when: ContextKeyExpr.and(
|
||||
// ChatContextKeys.supported,
|
||||
// ContextKeyExpr.has('config.chat.commandCenter.enabled')
|
||||
// ),
|
||||
order: 10001 // to the right of command center
|
||||
});
|
||||
|
||||
@@ -667,11 +669,13 @@ MenuRegistry.appendMenuItem(MenuId.TitleBar, {
|
||||
title: localize('title4', "Copilot"),
|
||||
group: 'navigation',
|
||||
icon: Codicon.copilot,
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.supported,
|
||||
ContextKeyExpr.has('config.chat.commandCenter.enabled'),
|
||||
ContextKeyExpr.has('config.window.commandCenter').negate(),
|
||||
),
|
||||
when: ContextKeyExpr.false(),
|
||||
// Void commented this out - copilot head
|
||||
// when: ContextKeyExpr.and(
|
||||
// ChatContextKeys.supported,
|
||||
// ContextKeyExpr.has('config.chat.commandCenter.enabled'),
|
||||
// ContextKeyExpr.has('config.window.commandCenter').negate(),
|
||||
// ),
|
||||
order: 1
|
||||
}); */
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
@@ -14,15 +15,16 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
|
||||
|
||||
// to change this, just Cmd+Shift+F and replace DummyService with YourServiceName, and create a unique ID below
|
||||
export interface IDummyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly _serviceBrand: undefined; // services need this, just leave it undefined
|
||||
}
|
||||
|
||||
export const IDummyService = createDecorator<IDummyService>('DummyService');
|
||||
|
||||
|
||||
|
||||
|
||||
// An example of an action (delete if you're not using an action):
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -42,18 +44,21 @@ registerAction2(class extends Action2 {
|
||||
}
|
||||
})
|
||||
|
||||
// on mount
|
||||
|
||||
class DummyService extends Disposable implements IWorkbenchContribution, IDummyService {
|
||||
static readonly ID = 'workbench.contrib.void.dummy'
|
||||
static readonly ID = 'workbench.contrib.void.dummy' // workbenchContributions need this, services do not
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// pick one and delete the other:
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
|
||||
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -791,18 +792,21 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
const featureName: FeatureName = 'Autocomplete'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const aiInstructions = this._settingsService.state.globalSettings.aiInstructions
|
||||
|
||||
// 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,
|
||||
},
|
||||
aiInstructions
|
||||
}),
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
@@ -890,6 +894,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,11 +11,10 @@ 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 { IToolsService } from './toolsService.js';
|
||||
@@ -23,7 +22,6 @@ 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';
|
||||
@@ -33,11 +31,35 @@ 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 { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
@@ -221,17 +243,14 @@ 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
|
||||
@@ -288,14 +307,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}
|
||||
|
||||
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
|
||||
]
|
||||
// remove all old selectons that are marked as `wasAddedAsCurrentFile`
|
||||
const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => s.state && !s.state.wasAddedAsCurrentFile)
|
||||
|
||||
const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath)
|
||||
|
||||
if (!fileIsAlreadyHere) {
|
||||
newStagingSelections.push(newStagingSelection)
|
||||
}
|
||||
|
||||
this.setCurrentThreadState({ stagingSelections: newStagingSelections });
|
||||
|
||||
}
|
||||
@@ -454,10 +475,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}
|
||||
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 +503,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 +526,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,7 +546,7 @@ 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
|
||||
@@ -593,7 +557,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
if (toolRequiresApproval) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove
|
||||
// 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: '(never)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
@@ -603,9 +567,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 {
|
||||
@@ -616,14 +582,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
delete this._currentlyRunningToolInterruptor[threadId];
|
||||
}
|
||||
toolResult = await result // ts is bad... await is needed
|
||||
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
}
|
||||
catch (error) {
|
||||
if (interrupted) {
|
||||
// the tool result is added when we stop running
|
||||
return { interrupted: true }
|
||||
}
|
||||
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 {}
|
||||
}
|
||||
|
||||
@@ -632,12 +599,12 @@ 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 })
|
||||
|
||||
return {}
|
||||
};
|
||||
@@ -658,6 +625,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
|
||||
|
||||
@@ -671,7 +639,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -687,34 +655,36 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
// 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 chatMessages = this.state.allThreads[threadId]?.messages ?? []
|
||||
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
|
||||
chatMessages,
|
||||
modelSelection,
|
||||
chatMode
|
||||
})
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
messages,
|
||||
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, 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: (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||
// 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._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
this._setStreamState(threadId, { error }, 'set')
|
||||
resMessageIsDonePromise()
|
||||
},
|
||||
@@ -741,7 +711,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
// 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 })
|
||||
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
|
||||
@@ -954,7 +924,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
const [_, toIdx] = c
|
||||
if (toIdx === fromIdx) return
|
||||
|
||||
console.log(`going from ${fromIdx} to ${toIdx}`)
|
||||
// console.log(`going from ${fromIdx} to ${toIdx}`)
|
||||
|
||||
// update the user's checkpoint
|
||||
this._addUserModificationsToCurrCheckpoint({ threadId })
|
||||
@@ -1049,6 +1019,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
severity: error ? Severity.Warning : Severity.Info,
|
||||
message: error ? `Error: ${error} ` : `A new Chat result is ready.`,
|
||||
source: messageContent,
|
||||
sticky: true,
|
||||
actions: {
|
||||
primary: [{
|
||||
id: 'void.goToChat',
|
||||
@@ -1485,9 +1456,9 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
}, true)
|
||||
|
||||
// when change focused message idx, jump
|
||||
if (messageIdx !== undefined)
|
||||
this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
|
||||
// // when change focused message idx, jump - do not jump back when click edit, too confusing.
|
||||
// if (messageIdx !== undefined)
|
||||
// this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
|
||||
}
|
||||
|
||||
// set message.state
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
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 { toolCallXMLStr, 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';
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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${toolCallXMLStr(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, aiInstructions: string, }): { 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,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
||||
// 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 === undefined
|
||||
|
||||
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]
|
||||
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
|
||||
|
||||
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]
|
||||
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
|
||||
|
||||
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, aiInstructions }) => {
|
||||
|
||||
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
|
||||
return { prefix, suffix, stopTokens }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// pick one and delete the other:
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -14,18 +14,23 @@ 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 { VoidDirectoryItem } from '../common/directoryStrTypes.js';
|
||||
import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
|
||||
|
||||
// const MAX_FILES_TOTAL = 200
|
||||
const MAX_FILES_TOTAL = 300;
|
||||
|
||||
const DEFAULT_MAX_DEPTH = 3;
|
||||
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
|
||||
|
||||
const START_MAX_DEPTH = Infinity;
|
||||
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
|
||||
|
||||
|
||||
export interface IDirectoryStrService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getDirectoryStrTool(uri: URI): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
|
||||
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
|
||||
|
||||
}
|
||||
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
|
||||
@@ -53,11 +58,17 @@ const shouldExcludeDirectory = (item: ExplorerItem) => {
|
||||
item.name === 'obj' ||
|
||||
item.name === 'vendor' ||
|
||||
item.name === 'logs' ||
|
||||
item.name === 'cache'
|
||||
item.name === 'cache' ||
|
||||
item.name === 'resource' ||
|
||||
item.name === 'resources'
|
||||
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.name.match(/\bout\b/)) return true
|
||||
if (item.name.match(/\bbuild\b/)) return true
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -128,130 +139,175 @@ export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], re
|
||||
|
||||
// ---------- IN GENERAL ----------
|
||||
|
||||
|
||||
// if the filter exists use it to filter out files and folders when creating the tree
|
||||
const computeDirectoryTree = async (
|
||||
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
|
||||
const computeAndStringifyDirectoryTree = async (
|
||||
eItem: ExplorerItem,
|
||||
explorerService: IExplorerService
|
||||
): Promise<VoidDirectoryItem> => {
|
||||
// Fetch children with default sort order
|
||||
const eChildren = await eItem.fetchChildren(SortOrder.FilesFirst);
|
||||
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem)
|
||||
|
||||
// Process children recursively
|
||||
const children = !isGitIgnoredDirectory ? await Promise.all(
|
||||
eChildren.map(async c => await computeDirectoryTree(c, explorerService))
|
||||
) : null
|
||||
|
||||
// Create our directory item
|
||||
const item: VoidDirectoryItem = {
|
||||
uri: eItem.resource,
|
||||
name: eItem.name,
|
||||
isDirectory: eItem.isDirectory,
|
||||
isSymbolicLink: eItem.isSymbolicLink,
|
||||
children,
|
||||
isGitIgnoredDirectory: isGitIgnoredDirectory && { numChildren: eItem.children.size },
|
||||
};
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
|
||||
const stringifyDirectoryTree = (
|
||||
node: VoidDirectoryItem,
|
||||
explorerService: IExplorerService,
|
||||
MAX_CHARS: number,
|
||||
): { content: string, wasCutOff: boolean } => {
|
||||
let content = '';
|
||||
let wasCutOff = false;
|
||||
fileCount: { count: number } = { count: 0 },
|
||||
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
|
||||
): Promise<{ content: string, wasCutOff: boolean }> => {
|
||||
// Set default values for options
|
||||
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
||||
const currentDepth = options.currentDepth ?? 0;
|
||||
const maxItemsPerDir = options.maxItemsPerDir ?? DEFAULT_MAX_ITEMS_PER_DIR;
|
||||
|
||||
// Check if we've reached the max depth
|
||||
if (currentDepth > maxDepth) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// Check if we've reached the file limit
|
||||
if (fileCount.count >= MAX_FILES_TOTAL) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// If we're already exceeding the max characters, return immediately
|
||||
if (MAX_CHARS <= 0) {
|
||||
return { content, wasCutOff: true };
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// Increment file count
|
||||
fileCount.count++;
|
||||
|
||||
// Add the root node first (without tree characters)
|
||||
const nodeLine = `${node.name}${node.isDirectory ? '/' : ''}${node.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
const nodeLine = `${eItem.name}${eItem.isDirectory ? '/' : ''}${eItem.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
|
||||
if (nodeLine.length > MAX_CHARS) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
content += nodeLine;
|
||||
let content = nodeLine;
|
||||
let wasCutOff = false;
|
||||
let remainingChars = MAX_CHARS - nodeLine.length;
|
||||
|
||||
// Then recursively add all children with proper tree formatting
|
||||
if (node.children && node.children.length > 0) {
|
||||
const { childrenContent, childrenCutOff } = renderChildren(
|
||||
node.children,
|
||||
remainingChars,
|
||||
''
|
||||
);
|
||||
content += childrenContent;
|
||||
wasCutOff = childrenCutOff;
|
||||
// Check if it's a directory we should skip
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
|
||||
|
||||
// Fetch and process children if not a filtered directory
|
||||
if (eItem.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
|
||||
|
||||
// Then recursively add all children with proper tree formatting
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
const { childrenContent, childrenCutOff } = await renderChildrenCombined(
|
||||
eChildren,
|
||||
remainingChars,
|
||||
'',
|
||||
explorerService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
|
||||
);
|
||||
content += childrenContent;
|
||||
wasCutOff = childrenCutOff;
|
||||
}
|
||||
}
|
||||
|
||||
return { content, wasCutOff };
|
||||
};
|
||||
|
||||
// Helper function to render children with proper tree formatting
|
||||
const renderChildren = (
|
||||
children: VoidDirectoryItem[],
|
||||
const renderChildrenCombined = async (
|
||||
children: ExplorerItem[],
|
||||
maxChars: number,
|
||||
parentPrefix: string
|
||||
): { childrenContent: string, childrenCutOff: boolean } => {
|
||||
parentPrefix: string,
|
||||
explorerService: IExplorerService,
|
||||
fileCount: { count: number },
|
||||
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
|
||||
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
|
||||
const { maxDepth, currentDepth } = options; // Remove maxItemsPerDir from destructuring
|
||||
// Get maxItemsPerDir separately and make sure we use it
|
||||
// For first level (currentDepth = 0), always use Infinity regardless of what was passed
|
||||
const maxItemsPerDir = currentDepth === 0 ?
|
||||
Infinity :
|
||||
(options.maxItemsPerDir ?? DEFAULT_MAX_ITEMS_PER_DIR);
|
||||
const nextDepth = currentDepth + 1;
|
||||
|
||||
let childrenContent = '';
|
||||
let childrenCutOff = false;
|
||||
let remainingChars = maxChars;
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
const isLast = i === children.length - 1;
|
||||
// Check if we've reached max depth
|
||||
if (nextDepth > maxDepth) {
|
||||
return { childrenContent: '', childrenCutOff: true };
|
||||
}
|
||||
|
||||
// Apply maxItemsPerDir limit - only process the specified number of items
|
||||
const itemsToProcess = maxItemsPerDir === Infinity ? children : children.slice(0, maxItemsPerDir);
|
||||
const hasMoreItems = children.length > itemsToProcess.length;
|
||||
|
||||
for (let i = 0; i < itemsToProcess.length; i++) {
|
||||
// Check if we've reached the file limit
|
||||
if (fileCount.count >= MAX_FILES_TOTAL) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const child = itemsToProcess[i];
|
||||
const isLast = (i === itemsToProcess.length - 1) && !hasMoreItems;
|
||||
|
||||
// Create the tree branch symbols
|
||||
const branchSymbol = isLast ? '└── ' : '├── ';
|
||||
const childLine = `${parentPrefix}${branchSymbol}${child.name}${child.isDirectory ? '/' : ''}${child.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
|
||||
// Check if adding this line would exceed the limit
|
||||
if (childrenContent.length + childLine.length > maxChars) {
|
||||
if (childLine.length > remainingChars) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
|
||||
childrenContent += childLine;
|
||||
remainingChars -= childLine.length;
|
||||
fileCount.count++;
|
||||
|
||||
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
|
||||
|
||||
|
||||
// if gitignored, just say the number of children
|
||||
if (child.isDirectory && child.isGitIgnoredDirectory && child.isGitIgnoredDirectory.numChildren > 0) {
|
||||
childrenContent += `${nextLevelPrefix}└── ... (${child.isGitIgnoredDirectory.numChildren} children) ...\n`
|
||||
}
|
||||
// Skip processing children for git ignored directories
|
||||
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
|
||||
|
||||
// Create the prefix for the next level (continuation line or space)
|
||||
else if (child.children && child.children.length > 0) {
|
||||
if (child.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await child.fetchChildren(SortOrder.Modified);
|
||||
|
||||
const {
|
||||
childrenContent: grandChildrenContent,
|
||||
childrenCutOff: grandChildrenCutOff
|
||||
} = renderChildren(
|
||||
child.children,
|
||||
maxChars,
|
||||
nextLevelPrefix
|
||||
);
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
const {
|
||||
childrenContent: grandChildrenContent,
|
||||
childrenCutOff: grandChildrenCutOff
|
||||
} = await renderChildrenCombined(
|
||||
eChildren,
|
||||
remainingChars,
|
||||
nextLevelPrefix,
|
||||
explorerService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
|
||||
);
|
||||
|
||||
// If adding grandchildren content would exceed the limit
|
||||
if (childrenContent.length + grandChildrenContent.length > maxChars) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
if (grandChildrenContent.length > 0) {
|
||||
childrenContent += grandChildrenContent;
|
||||
remainingChars -= grandChildrenContent.length;
|
||||
}
|
||||
|
||||
childrenContent += grandChildrenContent;
|
||||
|
||||
if (grandChildrenCutOff) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
if (grandChildrenCutOff) {
|
||||
childrenCutOff = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a message if we truncated the items due to maxItemsPerDir
|
||||
if (hasMoreItems) {
|
||||
const remainingCount = children.length - itemsToProcess.length;
|
||||
const truncatedLine = `${parentPrefix}└── (${remainingCount} more items not shown...)\n`;
|
||||
|
||||
if (truncatedLine.length <= remainingChars) {
|
||||
childrenContent += truncatedLine;
|
||||
remainingChars -= truncatedLine.length;
|
||||
}
|
||||
childrenCutOff = true;
|
||||
}
|
||||
|
||||
return { childrenContent, childrenCutOff };
|
||||
};
|
||||
|
||||
@@ -269,12 +325,37 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
super();
|
||||
}
|
||||
|
||||
async getDirectoryStrTool(uri: URI) {
|
||||
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}.`)
|
||||
|
||||
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
|
||||
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_TOOL);
|
||||
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
|
||||
// First try with START_MAX_DEPTH
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
|
||||
);
|
||||
|
||||
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
|
||||
let content, wasCutOff;
|
||||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
);
|
||||
content = result.content;
|
||||
wasCutOff = result.wasCutOff;
|
||||
} else {
|
||||
content = initialContent;
|
||||
wasCutOff = initialCutOff;
|
||||
}
|
||||
|
||||
let c = content.substring(0, MAX_DIRSTR_CHARS_TOTAL_TOOL)
|
||||
c = `Directory of ${uri.fsPath}:\n${content}`
|
||||
@@ -283,13 +364,16 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
return c
|
||||
}
|
||||
|
||||
async getAllDirectoriesStr({ cutOffMessage }: { cutOffMessage: string }) {
|
||||
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
|
||||
let str: string = '';
|
||||
let cutOff = false;
|
||||
const folders = this.workspaceContextService.getWorkspace().folders;
|
||||
if (folders.length === 0)
|
||||
return '(NO WORKSPACE OPEN)';
|
||||
|
||||
// Use START_MAX_ITEMS_PER_DIR if not specified
|
||||
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
|
||||
|
||||
for (let i = 0; i < folders.length; i += 1) {
|
||||
if (i > 0) str += '\n';
|
||||
|
||||
@@ -301,9 +385,32 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
const eRoot = this.explorerService.findClosestRoot(rootURI);
|
||||
if (!eRoot) continue;
|
||||
|
||||
// Use our new approach with direct explorer service
|
||||
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
|
||||
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length);
|
||||
// First try with START_MAX_DEPTH and startMaxItemsPerDir
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
|
||||
);
|
||||
|
||||
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
|
||||
let content, wasCutOff;
|
||||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
);
|
||||
content = result.content;
|
||||
wasCutOff = result.wasCutOff;
|
||||
} else {
|
||||
content = initialContent;
|
||||
wasCutOff = initialCutOff;
|
||||
}
|
||||
|
||||
str += content;
|
||||
if (wasCutOff) {
|
||||
cutOff = true;
|
||||
@@ -314,6 +421,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
if (cutOff) {
|
||||
return `${str}\n${cutOffMessage}`
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ 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';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
@@ -202,6 +203,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
// @IFileService private readonly _fileService: IFileService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -1233,7 +1235,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 +1268,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 +1305,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
|
||||
@@ -1315,12 +1324,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const startLine = startRange === 'fullFile' ? 1 : startRange[0]
|
||||
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, isOllamaFIM: false, fimTags: quickEditFIMTags, language })
|
||||
// type: 'messages',
|
||||
messages = [
|
||||
{ role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language })
|
||||
|
||||
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`) }
|
||||
|
||||
@@ -1384,10 +1398,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 +1420,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText: fullText_ } = params
|
||||
@@ -1485,6 +1496,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 +1512,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
|
||||
@@ -1593,10 +1610,6 @@ 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
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
@@ -1628,6 +1641,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
@@ -1682,7 +1696,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
console.log('---------')
|
||||
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -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' })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -57,7 +57,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 })
|
||||
@@ -120,11 +120,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 +152,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 +179,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,7 +188,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
if (t.type === 'table') {
|
||||
return (
|
||||
<div>
|
||||
<table>
|
||||
@@ -217,14 +217,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 +237,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 +251,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 +287,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
if (t.type === 'paragraph') {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index}
|
||||
@@ -304,34 +304,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 +334,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 +357,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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,14 +20,15 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
|
||||
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X } from 'lucide-react';
|
||||
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag } from 'lucide-react';
|
||||
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
|
||||
import { LintErrorItem, ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
|
||||
import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { IsRunningType } from '../../../chatThreadService.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { PlacesType } from 'react-tooltip';
|
||||
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
|
||||
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
|
||||
|
||||
|
||||
|
||||
@@ -660,7 +661,8 @@ type ToolHeaderParams = {
|
||||
children?: React.ReactNode;
|
||||
bottomChildren?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
isOpen?: boolean,
|
||||
isOpen?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ToolHeaderWrapper = ({
|
||||
@@ -671,10 +673,12 @@ const ToolHeaderWrapper = ({
|
||||
numResults,
|
||||
hasNextPage,
|
||||
children,
|
||||
bottomChildren,
|
||||
isError,
|
||||
onClick,
|
||||
isOpen,
|
||||
isRejected,
|
||||
className, // applies to the main content
|
||||
}: ToolHeaderParams) => {
|
||||
|
||||
const [isOpen_, setIsOpen] = useState(false);
|
||||
@@ -684,7 +688,7 @@ const ToolHeaderWrapper = ({
|
||||
const isClickable = !!(isDropdown || onClick)
|
||||
|
||||
return (<div className=''>
|
||||
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ">
|
||||
<div className={`w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ${className}`}>
|
||||
{/* header */}
|
||||
<div className={`select-none flex items-center min-h-[24px] ${!isDropdown ? 'mx-1' : ''}`}>
|
||||
<div className={`flex items-center w-full gap-x-2 overflow-hidden justify-between ${isRejected ? 'line-through' : ''}`}>
|
||||
@@ -733,6 +737,7 @@ const ToolHeaderWrapper = ({
|
||||
{children}
|
||||
</div>}
|
||||
</div>
|
||||
{bottomChildren}
|
||||
</div>);
|
||||
};
|
||||
|
||||
@@ -1167,11 +1172,12 @@ const titleOfToolName = {
|
||||
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
|
||||
'get_dir_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
|
||||
'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') },
|
||||
'search_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') },
|
||||
'search_for_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') },
|
||||
'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) },
|
||||
'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) },
|
||||
'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') },
|
||||
'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }
|
||||
'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') },
|
||||
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
|
||||
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
|
||||
|
||||
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type'>): React.ReactNode => {
|
||||
@@ -1200,8 +1206,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
|
||||
} else if (toolName === 'search_pathnames_only') {
|
||||
const toolParams = _toolParams as ToolCallParams['search_pathnames_only']
|
||||
return `"${toolParams.queryStr}"`;
|
||||
} else if (toolName === 'search_files') {
|
||||
const toolParams = _toolParams as ToolCallParams['search_files']
|
||||
} else if (toolName === 'search_for_files') {
|
||||
const toolParams = _toolParams as ToolCallParams['search_for_files']
|
||||
return `"${toolParams.queryStr}"`;
|
||||
} else if (toolName === 'create_file_or_folder') {
|
||||
const toolParams = _toolParams as ToolCallParams['create_file_or_folder']
|
||||
@@ -1292,7 +1298,7 @@ const ToolRequestAcceptRejectButtons = () => {
|
||||
</div>
|
||||
)
|
||||
|
||||
return <div className="flex gap-2 my-1 items-center">
|
||||
return <div className="flex gap-2 mx-4 items-center">
|
||||
{approveButton}
|
||||
{cancelButton}
|
||||
{autoApproveToggle}
|
||||
@@ -1330,7 +1336,7 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
|
||||
|
||||
|
||||
|
||||
const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => {
|
||||
const EditToolChildren = ({ uri, changeDescription }: { uri: URI | undefined, changeDescription: string }) => {
|
||||
return <div className='!select-text cursor-auto'>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
|
||||
@@ -1338,6 +1344,44 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
|
||||
return <div className="text-xs text-void-fg-4 opacity-80 border-l-2 border-void-warning px-2 py-0.5 flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap">
|
||||
{lintErrors.map((error, i) => (
|
||||
<div key={i}>Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
|
||||
if (lintErrors.length === 0) return null;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<div className="w-full px-2 mt-0.5">
|
||||
<div
|
||||
className={`flex items-center cursor-pointer select-none transition-colors duration-150 pl-0 py-0.5 rounded group`}
|
||||
onClick={() => setIsOpen(o => !o)}
|
||||
style={{ background: 'none' }}
|
||||
>
|
||||
<ChevronRight
|
||||
className={`mr-1 h-4 w-4 flex-shrink-0 transition-transform duration-100 text-void-fg-4 group-hover:text-void-fg-3 ${isOpen ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
<span className="font-medium text-void-fg-4 group-hover:text-void-fg-3 text-xs">Lint errors</span>
|
||||
</div>
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-200 ease-in-out ${isOpen ? 'opacity-100 py-1' : 'max-h-0 opacity-0'} text-xs pl-5`}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap text-void-fg-4 opacity-90 border-l-2 border-void-warning px-2 py-0.5">
|
||||
{lintErrors.map((error, i) => (
|
||||
<div key={i} className="">Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: string, uri: URI, codeStr: string }) => {
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
return <div className='flex items-center gap-1'>
|
||||
@@ -1399,11 +1443,18 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
const isError = toolMessage.type === 'tool_error'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
|
||||
if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) {
|
||||
const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}`
|
||||
const end = toolMessage.params.endLine === null ? `` : `${toolMessage.params.endLine}`
|
||||
const addStr = `(${start}-${end})`
|
||||
componentParams.desc1 += ` ${addStr}`
|
||||
}
|
||||
|
||||
if (toolMessage.type === 'success') {
|
||||
const { params, result } = toolMessage
|
||||
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
|
||||
if (result.hasNextPage && params.pageNumber === 1) // first page
|
||||
componentParams.desc2 = '(more content available)'
|
||||
componentParams.desc2 = `(truncated after ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)`
|
||||
else if (params.pageNumber > 1) // subsequent pages
|
||||
componentParams.desc2 = `(part ${params.pageNumber})`
|
||||
}
|
||||
@@ -1554,7 +1605,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
}
|
||||
},
|
||||
'search_files': {
|
||||
'search_for_files': {
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
@@ -1598,6 +1649,47 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
}
|
||||
},
|
||||
|
||||
'read_lint_errors': {
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const title = getTitle(toolMessage)
|
||||
|
||||
const { uri } = toolMessage.params ?? {}
|
||||
const desc1 = uri ? getBasename(uri.fsPath) : '';
|
||||
const icon = null
|
||||
|
||||
if (toolMessage.type === 'tool_request') return null
|
||||
if (toolMessage.type === 'rejected') return null // will never happen, not rejectable
|
||||
if (toolMessage.type === 'running_now') return null // do not show running
|
||||
|
||||
const isError = toolMessage.type === 'tool_error'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
|
||||
if (toolMessage.type === 'success') {
|
||||
const { params, result } = toolMessage
|
||||
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
|
||||
if (result.lintErrors)
|
||||
componentParams.children = <LintErrorChildren lintErrors={result.lintErrors} />
|
||||
else
|
||||
componentParams.children = `No lint errors found.`
|
||||
|
||||
}
|
||||
else if (toolMessage.type === 'tool_error') {
|
||||
const { params, result } = toolMessage
|
||||
if (params) componentParams.desc2 = <JumpToFileButton uri={params.uri} />
|
||||
componentParams.children = <ToolChildrenWrapper>
|
||||
<CodeChildren>
|
||||
{result}
|
||||
</CodeChildren>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
},
|
||||
},
|
||||
|
||||
// ---
|
||||
|
||||
'create_file_or_folder': {
|
||||
@@ -1726,7 +1818,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
if (toolMessage.type !== 'tool_error') {
|
||||
const { params, result } = toolMessage
|
||||
|
||||
// componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
|
||||
componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
|
||||
|
||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<EditToolChildren
|
||||
@@ -1842,28 +1934,30 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIsRunning }: { message: CheckpointEntry, threadId: string; messageIdx: number, isCheckpointGhost: boolean, threadIsRunning: boolean }) => {
|
||||
const accessor = useAccessor()
|
||||
const chatThreadService = accessor.get('IChatThreadService')
|
||||
const [showCheckpointIcon, setShowCheckpointIcon] = React.useState(false); // add icon state
|
||||
|
||||
return <div
|
||||
className={`flex items-center justify-center px-2 `}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
text-xs
|
||||
text-void-fg-3
|
||||
cursor-pointer select-none
|
||||
${isCheckpointGhost ? 'opacity-50' : 'opacity-100'}
|
||||
`}
|
||||
text-xs
|
||||
text-void-fg-3
|
||||
cursor-pointer select-none
|
||||
${isCheckpointGhost ? 'opacity-50' : 'opacity-100'}
|
||||
`}
|
||||
style={{ position: 'relative', display: 'inline-block' }} // allow absolute icon
|
||||
onClick={() => {
|
||||
if (threadIsRunning) return
|
||||
chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
|
||||
}}
|
||||
>
|
||||
Checkpoint
|
||||
Checkpoint
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit'
|
||||
type ChatBubbleProps = {
|
||||
chatMessage: ChatMessage,
|
||||
@@ -2260,6 +2354,37 @@ const CommandBarInChat = () => {
|
||||
}
|
||||
|
||||
|
||||
const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => {
|
||||
|
||||
const uri = URI.file(toolCallSoFar.rawParams.uri ?? 'unknown')
|
||||
|
||||
const title = titleOfToolName['edit_file'].proposed
|
||||
|
||||
const uriDone = toolCallSoFar.doneParams.includes('uri')
|
||||
const desc1 = <span className='flex items-center'>
|
||||
{uriDone ?
|
||||
getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown')
|
||||
: `Generating`}
|
||||
<IconLoading />
|
||||
</span>
|
||||
|
||||
// If URI has not been specified
|
||||
return <ToolHeaderWrapper
|
||||
title={title}
|
||||
desc1={desc1}
|
||||
desc2={<JumpToFileButton uri={uri} />}
|
||||
>
|
||||
<EditToolChildren
|
||||
uri={uri}
|
||||
changeDescription={toolCallSoFar.rawParams.change_description ?? ''}
|
||||
/>
|
||||
<IconLoading />
|
||||
</ToolHeaderWrapper>
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
export const SidebarChat = () => {
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
@@ -2379,7 +2504,6 @@ export const SidebarChat = () => {
|
||||
role: 'assistant',
|
||||
displayContent: displayContentSoFar ?? '',
|
||||
reasoning: reasoningSoFar ?? '',
|
||||
toolCall: toolCallSoFar,
|
||||
anthropicReasoning: null,
|
||||
}}
|
||||
messageIdx={streamingChatIdx}
|
||||
@@ -2391,6 +2515,15 @@ export const SidebarChat = () => {
|
||||
/> : null
|
||||
|
||||
|
||||
// the tool currently being generated
|
||||
const generatingTool = toolIsGenerating ?
|
||||
toolCallSoFar.name === 'edit_file' ? <EditToolSoFar
|
||||
key={getChatBubbleId(threadId, streamingChatIdx + 1)}
|
||||
toolCallSoFar={toolCallSoFar}
|
||||
/>
|
||||
: null
|
||||
: null
|
||||
|
||||
const messagesHTML = <ScrollToBottomContainer
|
||||
key={'messages' + chatThreadsState.currentThreadId} // force rerender on all children if id changes
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
@@ -2407,17 +2540,11 @@ export const SidebarChat = () => {
|
||||
{previousMessagesHTML}
|
||||
{currStreamingMessageHTML}
|
||||
|
||||
{toolIsGenerating ?
|
||||
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)}
|
||||
title={toolCallSoFar && toolNames.includes(toolCallSoFar.name as ToolName) ?
|
||||
titleOfToolName[toolCallSoFar.name as ToolName]?.proposed
|
||||
: toolCallSoFar?.name}
|
||||
desc1={<span className='flex items-center'>Generating<IconLoading /></span>}
|
||||
/>
|
||||
: null}
|
||||
{/* Generating tool */}
|
||||
{generatingTool}
|
||||
|
||||
{/* loading indicator */}
|
||||
{isRunning === 'LLM' && !toolIsGenerating ? <ProseWrapper>
|
||||
{/* loading indicator */}
|
||||
{<IconLoading className='opacity-50 text-sm' />}
|
||||
</ProseWrapper> : null}
|
||||
|
||||
|
||||
@@ -418,6 +418,7 @@ export const VoidSwitch = ({
|
||||
onChange,
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
...props
|
||||
}: {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
@@ -425,7 +426,7 @@ export const VoidSwitch = ({
|
||||
size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md';
|
||||
}) => {
|
||||
return (
|
||||
<label className="inline-flex items-center">
|
||||
<label className="inline-flex items-center" {...props}>
|
||||
<div
|
||||
onClick={() => !disabled && onChange(!value)}
|
||||
className={`
|
||||
|
||||
@@ -47,6 +47,7 @@ import { IWorkspaceContextService } from '../../../../../../../platform/workspac
|
||||
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
|
||||
import { INativeHostService } from '../../../../../../../platform/native/common/native.js';
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
import { IToolsService } from '../../../toolsService.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
@@ -215,6 +216,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
|
||||
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||
INativeHostService: accessor.get(INativeHostService),
|
||||
IToolsService: accessor.get(IToolsService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
|
||||
+2
-2
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,747 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
|
||||
import { Check, ExternalLink, 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';
|
||||
|
||||
const OVERRIDE_VALUE = true
|
||||
|
||||
export const VoidOnboarding = () => {
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete || OVERRIDE_VALUE
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return (
|
||||
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div
|
||||
className={`
|
||||
hidden
|
||||
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
|
||||
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
|
||||
`}
|
||||
>
|
||||
<VoidOnboardingContent />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const FADE_DURATION_MS = 2000
|
||||
|
||||
|
||||
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
|
||||
const [opacity, setOpacity] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setOpacity(1)
|
||||
}, delayMs)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [setOpacity, delayMs])
|
||||
|
||||
|
||||
return (
|
||||
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Onboarding
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
// "Welcome to Void"
|
||||
// image
|
||||
// content:<></>
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
// "How would you like to use Void?"
|
||||
// content:
|
||||
// ModelQuestionContent
|
||||
// |
|
||||
// div
|
||||
// "I want to:"
|
||||
// div
|
||||
// "Use the smartest models"
|
||||
// "Keep my data fully private"
|
||||
// "Save money"
|
||||
// "I don't know"
|
||||
// | div
|
||||
// | div
|
||||
// "We recommend using "
|
||||
// "Set API"
|
||||
// | div
|
||||
// ""
|
||||
// | div
|
||||
//
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
//
|
||||
// OnboardingPage
|
||||
// title
|
||||
// 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"
|
||||
{...props.disabled && {
|
||||
'data-tooltip-id': 'void-tooltip',
|
||||
'data-tooltip-content': 'Disabled (Please enter all required fields or choose another Provider)',
|
||||
'data-tooltip-place': 'top',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
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"
|
||||
{...props}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, className = '', }: {
|
||||
top?: React.ReactNode,
|
||||
bottom?: React.ReactNode,
|
||||
content?: React.ReactNode,
|
||||
hasMaxWidth?: boolean,
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
|
||||
|
||||
|
||||
// for now just link to the ollama download page
|
||||
return <a
|
||||
href={`https://ollama.com/library/${modelName}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
|
||||
// if (isModelInstalled) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Uninstall</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Trash}
|
||||
// onClick={() => {
|
||||
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// else if (isModelInstalling) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Square}
|
||||
// onClick={() => {
|
||||
// // abort()
|
||||
|
||||
// // TODO!!!!!!!!!!! don't do this
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
// else if (!isModelInstalled) {
|
||||
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Download}
|
||||
// onClick={() => {
|
||||
// // this is a check for whether the model was installed:
|
||||
|
||||
// if (isModelInstalling) return
|
||||
|
||||
|
||||
// // TODO!!!!!! don't do this
|
||||
|
||||
|
||||
// // install(modelname), callback = setIsModelInstalling(false);
|
||||
|
||||
// setIsModelInstalling(true);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
|
||||
// }
|
||||
|
||||
// return <></>
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
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 ? "Yes"
|
||||
: val === false ? 'No'
|
||||
: "Yes*"
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const abbreviateNumber = (num: number): string => {
|
||||
if (num >= 1000000) {
|
||||
// For millions
|
||||
return Math.floor(num / 1000000) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
// For thousands
|
||||
return Math.floor(num / 1000) + 'K';
|
||||
} else {
|
||||
// For numbers less than 1000
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
|
||||
// const providerCapabilities = getProviderCapabilities(providerName)
|
||||
|
||||
|
||||
// info used to show the table
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
|
||||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.isDefault,
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
|
||||
// special case columns for ollama; show recommended models as default
|
||||
if (providerName === 'ollama') {
|
||||
for (const modelName of ollamaRecommendedModels) {
|
||||
if (modelName in infoOfModelName) continue
|
||||
infoOfModelName[modelName] = {
|
||||
...infoOfModelName[modelName],
|
||||
showAsDefault: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
|
||||
<thead>
|
||||
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
|
||||
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
|
||||
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
|
||||
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(infoOfModelName).map(modelName => {
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
|
||||
|
||||
|
||||
const capabilities = getModelCapabilities(providerName, modelName)
|
||||
const {
|
||||
downloadable,
|
||||
cost,
|
||||
supportsFIM,
|
||||
reasoningCapabilities,
|
||||
contextWindow,
|
||||
|
||||
isUnrecognizedModel,
|
||||
maxOutputTokens,
|
||||
supportsSystemMessage,
|
||||
} = 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"
|
||||
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 relative">
|
||||
{!showAsDefault && removeModelButton}
|
||||
{modelName}
|
||||
</td>
|
||||
<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={!!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>}
|
||||
{providerName === 'ollama' && <th className="py-2 px-3">
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
</th>}
|
||||
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<tr className="hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 text-void-accent">
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true} />
|
||||
</td>
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
|
||||
|
||||
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
|
||||
|
||||
const VoidOnboardingContent = () => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0)
|
||||
|
||||
|
||||
// page 1 state
|
||||
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
|
||||
|
||||
// page 2 state
|
||||
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
all: providerNames,
|
||||
// TODO allow user to redo onboarding
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
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">
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
<NextButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
|
||||
/>
|
||||
</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.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// can be md
|
||||
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.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// set the selected provider name appropriately
|
||||
useEffect(() => {
|
||||
if (wantToUseOption && providerNamesOfWantToUseOption[wantToUseOption].length > 0) {
|
||||
setSelectedProviderName(providerNamesOfWantToUseOption[wantToUseOption][0]);
|
||||
} else {
|
||||
setSelectedProviderName(null);
|
||||
}
|
||||
}, [wantToUseOption]);
|
||||
|
||||
// set wantToUseOption to smart when page changes
|
||||
useEffect(() => {
|
||||
setWantToUseOption(wantToUseOption);
|
||||
}, [pageIndex]);
|
||||
|
||||
|
||||
// reset the page to page 0 if the user redos onboarding
|
||||
useEffect(() => {
|
||||
if (!voidSettingsState.globalSettings.isOnboardingComplete) {
|
||||
setPageIndex(0)
|
||||
}
|
||||
}, [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={
|
||||
''
|
||||
}
|
||||
/>,
|
||||
1: <OnboardingPageShell
|
||||
hasMaxWidth={false}
|
||||
top={
|
||||
<FadeIn className='flex flex-col items-center'>
|
||||
<div className="text-5xl font-light text-center">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="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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</FadeIn>
|
||||
}
|
||||
content={<></>}
|
||||
/>,
|
||||
2: <OnboardingPageShell
|
||||
top={
|
||||
<div className='flex flex-col items-center'>
|
||||
{/* Title */}
|
||||
<div className="text-5xl font-light text-center">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>
|
||||
|
||||
|
||||
{/* 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
|
||||
|
||||
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>
|
||||
)
|
||||
})}
|
||||
|
||||
</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>
|
||||
|
||||
|
||||
{/* ModelsTable and ProviderFields */}
|
||||
{selectedProviderName && <div className='mt-4'>
|
||||
|
||||
|
||||
{/* Models Table */}
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
|
||||
|
||||
{/* Add provider section - simplified styling */}
|
||||
<div className='mb-5 mt-8'>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
|
||||
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
|
||||
|
||||
</div>
|
||||
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
|
||||
{/* 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>}
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
bottom={
|
||||
prevAndNextButtons
|
||||
}
|
||||
|
||||
/>,
|
||||
|
||||
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
// <FadeIn>
|
||||
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
|
||||
|
||||
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
|
||||
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
|
||||
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
|
||||
|
||||
// </div>
|
||||
// </FadeIn>
|
||||
|
||||
// {prevAndNextButtons}
|
||||
// </div>,
|
||||
3: <OnboardingPageShell
|
||||
top={
|
||||
<div>
|
||||
<div className="text-5xl font-light text-center">Settings and Themes</div>
|
||||
|
||||
<div className="mt-8 text-center flex flex-col items-center gap-4 w-full max-w-md mx-auto">
|
||||
<h4 className="text-void-fg-3 mb-4">Transfer your settings from an existing editor?</h4>
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Windsurf" />
|
||||
</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) }}
|
||||
/>
|
||||
}
|
||||
/>,
|
||||
}
|
||||
|
||||
|
||||
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]}
|
||||
</div>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { VoidOnboarding } from './VoidOnboarding.js'
|
||||
|
||||
export const mountVoidOnboarding = mountFnGenerator(VoidOnboarding)
|
||||
@@ -3,23 +3,19 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js'
|
||||
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 ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButtonBgDarken, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check, MoveRight, PlusCircle, MinusCircle, Download, Trash, StopCircle, Square, ExternalLink } from 'lucide-react'
|
||||
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
|
||||
import { X, RefreshCw, Loader2, Check, } from 'lucide-react'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { env } from '../../../../../../../base/common/process.js'
|
||||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
import { os } from '../../../../common/helpers/systemInfo.js'
|
||||
import { IconLoading, IconX } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { getModelCapabilities, getProviderCapabilities, ollamaRecommendedModels, VoidStaticModelInfo } from '../../../../common/modelCapabilities.js'
|
||||
import VoidImage from './VoidImage.js'
|
||||
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
|
||||
|
||||
|
||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||
@@ -100,7 +96,7 @@ const RefreshableModels = () => {
|
||||
|
||||
|
||||
|
||||
const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => {
|
||||
export const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => {
|
||||
const [dashOffset, setDashOffset] = useState(40);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -158,7 +154,7 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t
|
||||
|
||||
|
||||
// shows a providerName dropdown if no `providerName` is given
|
||||
const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
|
||||
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
@@ -166,19 +162,22 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showCheckmark, setShowCheckmark] = useState(false)
|
||||
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null)
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
|
||||
|
||||
const providerName = permanentProviderName ?? userChosenProviderName;
|
||||
|
||||
const [modelName, setModelName] = useState<string>('')
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
if (!providerName) { return null; }
|
||||
|
||||
const numModels = settingsState.settingsOfProvider[providerName].models.length
|
||||
|
||||
if (showCheckmark) {
|
||||
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return <div
|
||||
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
|
||||
@@ -207,9 +206,8 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
|
||||
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-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}
|
||||
/>
|
||||
}
|
||||
@@ -220,7 +218,7 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
compact={compact}
|
||||
className={'max-w-44'}
|
||||
className={'max-w-32'}
|
||||
/>
|
||||
|
||||
{/* add button */}
|
||||
@@ -244,7 +242,11 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
setIsOpen(false)
|
||||
setShowCheckmark(true)
|
||||
setTimeout(() => {
|
||||
setShowCheckmark(false)
|
||||
setIsOpen(false)
|
||||
}, 1500)
|
||||
setErrorString('')
|
||||
setModelName('')
|
||||
}}
|
||||
@@ -288,7 +290,16 @@ export const ModelDump = () => {
|
||||
|
||||
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
|
||||
|
||||
const providerTitle = displayInfoOfProviderName(providerName).title
|
||||
|
||||
const disabled = !providerEnabled
|
||||
const value = disabled ? false : !isHidden
|
||||
|
||||
const tooltipName = (
|
||||
disabled ? `Add ${providerTitle} to enable`
|
||||
: value === true ? 'Enabled'
|
||||
: 'Disabled'
|
||||
)
|
||||
|
||||
return <div key={`${modelName}${providerName}`}
|
||||
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
|
||||
@@ -296,18 +307,28 @@ export const ModelDump = () => {
|
||||
>
|
||||
{/* left part is width:full */}
|
||||
<div className={`flex-grow flex items-center gap-4`}>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
|
||||
<span className='w-fit truncate'>{modelName}</span>
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className='flex items-center gap-4'>
|
||||
<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` : ``)
|
||||
// }
|
||||
>
|
||||
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
||||
|
||||
<VoidSwitch
|
||||
value={disabled ? false : !isHidden}
|
||||
value={value}
|
||||
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
|
||||
disabled={disabled}
|
||||
size='sm'
|
||||
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='right'
|
||||
data-tooltip-content={tooltipName}
|
||||
/>
|
||||
|
||||
<div className={`w-5 flex items-center justify-center`}>
|
||||
@@ -402,7 +423,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
// </div >
|
||||
// }
|
||||
|
||||
const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
|
||||
export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
|
||||
@@ -524,6 +545,29 @@ 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>
|
||||
|
||||
|
||||
const RedoOnboardingButton = ({ className }: { className?: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
return <div
|
||||
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
|
||||
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', false) }}
|
||||
>
|
||||
See onboarding screen?
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const FeaturesTab = () => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
const accessor = useAccessor()
|
||||
@@ -534,7 +578,8 @@ export const FeaturesTab = () => {
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<ModelDump />
|
||||
<AddModelInputBox />
|
||||
<AddModelInputBox className='mt-4' compact />
|
||||
<RedoOnboardingButton className='mt-2 mb-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
@@ -570,7 +615,19 @@ export const FeaturesTab = () => {
|
||||
{/* 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='text-sm italic text-void-fg-3 mt-1 mb-4'>
|
||||
<span>
|
||||
Experimental. Only works with FIM models.
|
||||
</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]'
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Enable Switch */}
|
||||
@@ -642,6 +699,16 @@ export const FeaturesTab = () => {
|
||||
/>
|
||||
<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 ? 'Fix lint errors' : `Don't fix lint errors`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -809,7 +876,7 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
||||
}
|
||||
|
||||
|
||||
const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
|
||||
export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const fileService = accessor.get('IFileService')
|
||||
|
||||
@@ -891,7 +958,7 @@ const GeneralTab = () => {
|
||||
return <>
|
||||
<div className=''>
|
||||
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
<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" />
|
||||
@@ -904,7 +971,7 @@ const GeneralTab = () => {
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
<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') }}>
|
||||
@@ -931,7 +998,7 @@ const GeneralTab = () => {
|
||||
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Instructions to include on all AI requests.`}</h4>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`Instructions to include on all AI requests.`}</h4>
|
||||
<AIInstructionsBox />
|
||||
</div>
|
||||
|
||||
@@ -946,14 +1013,6 @@ export const Settings = () => {
|
||||
|
||||
const [tab, setTab] = useState<TabName>('models')
|
||||
|
||||
|
||||
const deleteme = false
|
||||
if (deleteme) {
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
|
||||
<VoidOnboarding />
|
||||
</div>
|
||||
}
|
||||
|
||||
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'>
|
||||
|
||||
@@ -1000,663 +1059,3 @@ export const Settings = () => {
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const FADE_DURATION_MS = 2000
|
||||
|
||||
|
||||
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
const [opacity, setOpacity] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setOpacity(1)
|
||||
}, delayMs)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [setOpacity, delayMs])
|
||||
|
||||
|
||||
return (
|
||||
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Onboarding
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
// "Welcome to Void"
|
||||
// image
|
||||
// content:<></>
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
// "How would you like to use Void?"
|
||||
// content:
|
||||
// ModelQuestionContent
|
||||
// |
|
||||
// div
|
||||
// "I want to:"
|
||||
// div
|
||||
// "Use the smartest models"
|
||||
// "Keep my data fully private"
|
||||
// "Save money"
|
||||
// "I don't know"
|
||||
// | div
|
||||
// | div
|
||||
// "We recommend using "
|
||||
// "Set API"
|
||||
// | div
|
||||
// ""
|
||||
// | div
|
||||
//
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
//
|
||||
// OnboardingPage
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
|
||||
|
||||
const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 rounded bg-void-accent hover:bg-void-accent/90 text-white"
|
||||
{...props}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
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"
|
||||
{...props}
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const PreviousButton = ({ 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"
|
||||
{...props}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ollamaSetupInstructions = <div className='prose-p:my-0 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 font-light list-decimal select-text opacity-80'>
|
||||
<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>
|
||||
|
||||
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
|
||||
|
||||
|
||||
// for now just link to the ollama download page
|
||||
return <a
|
||||
href={`https://ollama.com/library/${modelName}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
|
||||
// if (isModelInstalled) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Uninstall</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Trash}
|
||||
// onClick={() => {
|
||||
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// else if (isModelInstalling) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Square}
|
||||
// onClick={() => {
|
||||
// // abort()
|
||||
|
||||
// // TODO!!!!!!!!!!! don't do this
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
// else if (!isModelInstalled) {
|
||||
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Download}
|
||||
// onClick={() => {
|
||||
// // this is a check for whether the model was installed:
|
||||
|
||||
// if (isModelInstalling) return
|
||||
|
||||
|
||||
// // TODO!!!!!! don't do this
|
||||
|
||||
|
||||
// // install(modelname), callback = setIsModelInstalling(false);
|
||||
|
||||
// setIsModelInstalling(true);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
|
||||
// }
|
||||
|
||||
// return <></>
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
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 ? "Yes"
|
||||
: val === false ? 'No'
|
||||
: "Yes*"
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const abbreviateNumber = (num: number): string => {
|
||||
if (num >= 1000000) {
|
||||
// For millions
|
||||
return Math.floor(num / 1000000) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
// For thousands
|
||||
return Math.floor(num / 1000) + 'K';
|
||||
} else {
|
||||
// For numbers less than 1000
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
|
||||
// const providerCapabilities = getProviderCapabilities(providerName)
|
||||
|
||||
|
||||
// info used to show the table
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
|
||||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.isDefault,
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
|
||||
// special case columns for ollama; show recommended models as default
|
||||
if (providerName === 'ollama') {
|
||||
for (const modelName of ollamaRecommendedModels) {
|
||||
if (modelName in infoOfModelName) continue
|
||||
infoOfModelName[modelName] = {
|
||||
...infoOfModelName[modelName],
|
||||
showAsDefault: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
|
||||
<thead>
|
||||
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
|
||||
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
|
||||
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
|
||||
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(infoOfModelName).map(modelName => {
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
|
||||
|
||||
const {
|
||||
downloadable,
|
||||
cost,
|
||||
supportsTools,
|
||||
supportsFIM,
|
||||
reasoningCapabilities,
|
||||
contextWindow,
|
||||
|
||||
isUnrecognizedModel,
|
||||
maxOutputTokens,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
|
||||
|
||||
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"
|
||||
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 relative">
|
||||
{!showAsDefault && removeModelButton}
|
||||
{modelName}
|
||||
</td>
|
||||
<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 || null} /></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>}
|
||||
{providerName === 'ollama' && <th className="py-2 px-3">
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
</th>}
|
||||
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<tr className="hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 text-void-accent">
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true} />
|
||||
</td>
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
|
||||
|
||||
const VoidOnboarding = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isOnboardingComplete = false // voidSettingsService._isOnboardingComplete
|
||||
|
||||
if (isOnboardingComplete) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0)
|
||||
|
||||
|
||||
const skipButton = <SkipButton onClick={() => { setPageIndex(pageIndex + 1) }} />
|
||||
|
||||
|
||||
// page 1 state
|
||||
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
|
||||
|
||||
// page 2 state
|
||||
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
all: providerNames,
|
||||
// TODO allow user to redo onboarding
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
const didFillInSelectedProviderSettings = !!(didFillInProviderSettings && isApiKeyLongEnoughIfApiKeyExists && isAtLeastOneModel)
|
||||
|
||||
const prevAndNextButtons = <div className="self-end flex items-center gap-1 pb-8">
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
<NextButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
|
||||
/>
|
||||
</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 affordable options.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// can be md
|
||||
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 for free.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// set the selected provider name appropriately
|
||||
useEffect(() => {
|
||||
if (wantToUseOption && providerNamesOfWantToUseOption[wantToUseOption].length > 0) {
|
||||
setSelectedProviderName(providerNamesOfWantToUseOption[wantToUseOption][0]);
|
||||
} else {
|
||||
setSelectedProviderName(null);
|
||||
}
|
||||
}, [wantToUseOption]);
|
||||
|
||||
// set wantToUseOption to smart when page changes
|
||||
useEffect(() => {
|
||||
setWantToUseOption(wantToUseOption);
|
||||
}, [pageIndex]);
|
||||
|
||||
|
||||
// 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: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
<FadeIn >
|
||||
<div className="text-5xl font-light mb-6 mt-12 text-center">Welcome to Void</div>
|
||||
|
||||
|
||||
<div className="w-8 h-8 mb-2">
|
||||
<VoidImage className='h-full w-full' />
|
||||
</div>
|
||||
</FadeIn>
|
||||
|
||||
<FadeIn delayMs={1000} className="text-center pb-8" onClick={() => { setPageIndex(pageIndex + 1) }}>
|
||||
Get Started
|
||||
</FadeIn>
|
||||
</div>,
|
||||
1: <div className="max-w-full w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
|
||||
<FadeIn>
|
||||
|
||||
<div className="text-3xl font-medium mb-6 mt-8 text-center">AI Preferences</div>
|
||||
|
||||
<div className="flex flex-col items-center w-full mx-auto">
|
||||
|
||||
<div className="text-base text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 w-full md:max-w-[75%] max-w-[90%]">
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5"
|
||||
>
|
||||
<span className="text-5xl mb-4">🧠</span>
|
||||
<h3 className="text-xl font-medium mb-3">Intelligence</h3>
|
||||
<p className="text-center text-sm text-void-fg-2">{basicDescOfWantToUseOption['smart']}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5"
|
||||
>
|
||||
<span className="text-5xl mb-4">🔒</span>
|
||||
<h3 className="text-xl font-medium mb-3">Privacy</h3>
|
||||
<p className="text-center text-sm text-void-fg-2">{basicDescOfWantToUseOption['private']}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5"
|
||||
>
|
||||
<span className="text-5xl mb-4">💵</span>
|
||||
<h3 className="text-xl font-medium mb-3">Low-Cost</h3>
|
||||
<p className="text-center text-sm text-void-fg-2">{basicDescOfWantToUseOption['cheap']}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</FadeIn>
|
||||
|
||||
<div className="max-w-[600px] w-full flex flex-col items-center justify-between">
|
||||
{prevAndNextButtons}
|
||||
</div>
|
||||
|
||||
</div>,
|
||||
2: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
<FadeIn className="flex flex-col gap-2 w-full">
|
||||
|
||||
<div className="text-5xl font-light mb-6 mt-12 text-center">Choose a Provider</div>
|
||||
|
||||
<div className="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>
|
||||
|
||||
{/* Provider Buttons */}
|
||||
<div
|
||||
key={wantToUseOption}
|
||||
className="flex flex-wrap items-center mt-4 min-h-[37px] w-full"
|
||||
>
|
||||
|
||||
{(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => {
|
||||
const isSelected = selectedProviderName === providerName
|
||||
|
||||
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>
|
||||
)
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-left text-sm text-void-fg-3 px-2 py-1">
|
||||
|
||||
<div className='pl-4 select-text'>
|
||||
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{/* ModelsTable and ProviderFields */}
|
||||
{selectedProviderName && <div className='mt-4'>
|
||||
|
||||
|
||||
{/* Models Table */}
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
|
||||
|
||||
{/* Add provider section - simplified styling */}
|
||||
<div className='mb-5 mt-8'>
|
||||
<div className='text-base font-semibold'>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
|
||||
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
|
||||
|
||||
</div>
|
||||
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
|
||||
{/* 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>}
|
||||
</div>
|
||||
|
||||
|
||||
</div>}
|
||||
|
||||
</FadeIn>
|
||||
|
||||
{prevAndNextButtons}
|
||||
</div>,
|
||||
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
// <FadeIn>
|
||||
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
|
||||
|
||||
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
|
||||
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
|
||||
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
|
||||
|
||||
// </div>
|
||||
// </FadeIn>
|
||||
|
||||
// {prevAndNextButtons}
|
||||
// </div>,
|
||||
3: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
<FadeIn>
|
||||
<div className="text-5xl font-light mb-6 mt-12">Settings and Themes</div>
|
||||
|
||||
<div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
|
||||
<h4 className="text-void-fg-3 mb-2">Transfer your settings from an existing editor?</h4>
|
||||
<OneClickSwitchButton fromEditor="VS Code" />
|
||||
<OneClickSwitchButton fromEditor="Cursor" />
|
||||
<OneClickSwitchButton fromEditor="Windsurf" />
|
||||
</div>
|
||||
|
||||
</FadeIn>
|
||||
|
||||
{prevAndNextButtons}
|
||||
</div>,
|
||||
4: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
<FadeIn className="text-5xl font-light mb-6 mt-12">
|
||||
Jump in
|
||||
</FadeIn>
|
||||
|
||||
<FadeIn className="text-center">
|
||||
Enter the Void
|
||||
</FadeIn>
|
||||
|
||||
{prevAndNextButtons}
|
||||
</div>,
|
||||
}
|
||||
|
||||
|
||||
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-auto flex flex-col items-center justify-between">
|
||||
{contentOfIdx[pageIndex]}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const VoidTooltip = () => {
|
||||
font-size: 12px;
|
||||
padding: 0px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 999;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
#void-tooltip {
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
'./src2/sidebar-tsx/index.tsx',
|
||||
'./src2/void-settings-tsx/index.tsx',
|
||||
'./src2/void-tooltip/index.tsx',
|
||||
'./src2/void-onboarding/index.tsx',
|
||||
'./src2/quick-edit-tsx/index.tsx',
|
||||
'./src2/diff/index.tsx',
|
||||
],
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -228,9 +226,13 @@ registerAction2(class extends Action2 {
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
||||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
|
||||
// focus
|
||||
stateService.fireFocusChat()
|
||||
const window = getActiveWindow()
|
||||
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ 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 { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
@@ -25,7 +26,7 @@ import { ToolName } from '../common/prompt/prompts.js'
|
||||
|
||||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => Promise<ToolCallParams[T]> }
|
||||
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
||||
|
||||
@@ -33,7 +34,7 @@ type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awai
|
||||
|
||||
|
||||
// pagination info
|
||||
export const MAX_FILE_CHARS_PAGE = 50_000
|
||||
export const MAX_FILE_CHARS_PAGE = Infinity
|
||||
export const MAX_CHILDREN_URIs_PAGE = 500
|
||||
export const MAX_TERMINAL_CHARS_PAGE = 20_000
|
||||
export const TERMINAL_TIMEOUT_TIME = 5 // seconds
|
||||
@@ -151,34 +152,38 @@ export class ToolsService implements IToolsService {
|
||||
@IVoidCommandBarService private readonly commandBarService: IVoidCommandBarService,
|
||||
@IDirectoryStrService private readonly directoryStrService: IDirectoryStrService,
|
||||
@IMarkerService private readonly markerService: IMarkerService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.validateParams = {
|
||||
read_file: async (params: RawToolParamsObj) => {
|
||||
read_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, start_line: startLineUnknown, end_line: endLineUnknown, page_number: pageNumberUnknown } = params
|
||||
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 }
|
||||
},
|
||||
ls_dir: async (params: RawToolParamsObj) => {
|
||||
ls_dir: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, page_number: pageNumberUnknown } = params
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
return { rootURI: uri, pageNumber }
|
||||
},
|
||||
get_dir_structure: async (params: RawToolParamsObj) => {
|
||||
get_dir_structure: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, } = params
|
||||
const uri = validateURI(uriStr)
|
||||
return { rootURI: uri }
|
||||
},
|
||||
search_pathnames_only: async (params: RawToolParamsObj) => {
|
||||
search_pathnames_only: (params: RawToolParamsObj) => {
|
||||
const {
|
||||
query: queryUnknown,
|
||||
search_in_folder: includeUnknown,
|
||||
@@ -192,7 +197,7 @@ export class ToolsService implements IToolsService {
|
||||
return { queryStr, searchInFolder, pageNumber }
|
||||
|
||||
},
|
||||
search_files: async (params: RawToolParamsObj) => {
|
||||
search_for_files: (params: RawToolParamsObj) => {
|
||||
const {
|
||||
query: queryUnknown,
|
||||
search_in_folder: searchInFolderUnknown,
|
||||
@@ -209,9 +214,17 @@ export class ToolsService implements IToolsService {
|
||||
return { queryStr, searchInFolder, isRegex, pageNumber }
|
||||
},
|
||||
|
||||
read_lint_errors: (params: RawToolParamsObj) => {
|
||||
const {
|
||||
uri: uriUnknown,
|
||||
} = params
|
||||
const uri = validateURI(uriUnknown)
|
||||
return { uri }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
||||
create_file_or_folder: async (params: RawToolParamsObj) => {
|
||||
create_file_or_folder: (params: RawToolParamsObj) => {
|
||||
const { uri: uriUnknown } = params
|
||||
const uri = validateURI(uriUnknown)
|
||||
const uriStr = validateStr('uri', uriUnknown)
|
||||
@@ -219,7 +232,7 @@ export class ToolsService implements IToolsService {
|
||||
return { uri, isFolder }
|
||||
},
|
||||
|
||||
delete_file_or_folder: async (params: RawToolParamsObj) => {
|
||||
delete_file_or_folder: (params: RawToolParamsObj) => {
|
||||
const { uri: uriUnknown, params: paramsStr } = params
|
||||
const uri = validateURI(uriUnknown)
|
||||
const isRecursive = validateRecursiveParamStr(paramsStr)
|
||||
@@ -228,14 +241,14 @@ export class ToolsService implements IToolsService {
|
||||
return { uri, isRecursive, isFolder }
|
||||
},
|
||||
|
||||
edit_file: async (params: RawToolParamsObj) => {
|
||||
edit_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, change_description: changeDescriptionUnknown } = params
|
||||
const uri = validateURI(uriStr)
|
||||
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
|
||||
return { uri, changeDescription }
|
||||
},
|
||||
|
||||
run_terminal_command: async (params: RawToolParamsObj) => {
|
||||
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)
|
||||
@@ -266,8 +279,8 @@ 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 }) => {
|
||||
@@ -298,7 +311,7 @@ export class ToolsService implements IToolsService {
|
||||
return { result: { uris, hasNextPage } }
|
||||
},
|
||||
|
||||
search_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
|
||||
search_for_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
|
||||
const searchFolders = searchInFolder === null ?
|
||||
workspaceContextService.getWorkspace().folders.map(f => f.uri)
|
||||
: [searchInFolder]
|
||||
@@ -320,6 +333,12 @@ export class ToolsService implements IToolsService {
|
||||
return { result: { queryStr, uris, hasNextPage } }
|
||||
},
|
||||
|
||||
read_lint_errors: async ({ uri }) => {
|
||||
await timeout(1000)
|
||||
const { lintErrors } = this._getLintErrors(uri)
|
||||
return { result: { lintErrors } }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
||||
create_file_or_folder: async ({ uri, isFolder }) => {
|
||||
@@ -357,20 +376,11 @@ export class ToolsService implements IToolsService {
|
||||
editCodeService.interruptURIStreaming({ uri: diffZoneURI })
|
||||
}
|
||||
|
||||
// at end, get lint errors
|
||||
const lintErrorsPromise = applyDonePromise.then(async () => {
|
||||
await timeout(500)
|
||||
|
||||
const lintErrors = this.markerService
|
||||
.read({ resource: uri })
|
||||
.map(l => ({
|
||||
code: typeof l.code === 'string' ? l.code : l.code?.value || '',
|
||||
message: l.message,
|
||||
startLineNumber: l.startLineNumber,
|
||||
endLineNumber: l.endLineNumber,
|
||||
} satisfies LintErrorItem))
|
||||
|
||||
if (!lintErrors.length) return { lintErrors: null }
|
||||
return { lintErrors, }
|
||||
await timeout(2000)
|
||||
const { lintErrors } = this._getLintErrors(uri)
|
||||
return { lintErrors }
|
||||
})
|
||||
|
||||
return { result: lintErrorsPromise, interruptTool }
|
||||
@@ -384,12 +394,16 @@ export class ToolsService implements IToolsService {
|
||||
|
||||
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
|
||||
|
||||
const lintErrorsStr = (lintErrors: LintErrorItem[]) => lintErrors.map((e, i) => `Error ${i + 1}:\nLines Affected: ${e.startLineNumber}-${e.endLineNumber}\nError message:${e.message}`).join('\n\n')
|
||||
const stringifyLintErrors = (lintErrors: LintErrorItem[]) => {
|
||||
return lintErrors
|
||||
.map((e, i) => `Error ${i + 1}:\nLines Affected: ${e.startLineNumber}-${e.endLineNumber}\nError message:${e.message}`)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
// given to the LLM after the call
|
||||
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)
|
||||
@@ -401,9 +415,14 @@ export class ToolsService implements IToolsService {
|
||||
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)
|
||||
},
|
||||
read_lint_errors: (params, result) => {
|
||||
return result.lintErrors ?
|
||||
stringifyLintErrors(result.lintErrors)
|
||||
: 'No lint errors found.'
|
||||
},
|
||||
// ---
|
||||
create_file_or_folder: (params, result) => {
|
||||
return `URI ${params.uri.fsPath} successfully created.`
|
||||
@@ -412,10 +431,13 @@ export class ToolsService implements IToolsService {
|
||||
return `URI ${params.uri.fsPath} successfully deleted.`
|
||||
},
|
||||
edit_file: (params, result) => {
|
||||
const lintErrsString = (
|
||||
this.voidSettingsService.state.globalSettings.includeToolLintErrors ?
|
||||
(result.lintErrors ? ` Lint errors found after change:\n${stringifyLintErrors(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.`
|
||||
: ` No lint errors found.`)
|
||||
: '')
|
||||
|
||||
const additionalStr = result.lintErrors ? `Lint errors found after change:\n${lintErrorsStr(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` : `No lint errors found.`
|
||||
|
||||
return `Change successfully made to ${params.uri.fsPath}.${additionalStr}`
|
||||
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
|
||||
},
|
||||
run_terminal_command: (params, result) => {
|
||||
const {
|
||||
@@ -449,6 +471,22 @@ 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)
|
||||
.map(l => ({
|
||||
code: typeof l.code === 'string' ? l.code : l.code?.value || '',
|
||||
message: (l.severity === MarkerSeverity.Error ? '(error) ' : '(warning) ') + l.message,
|
||||
startLineNumber: l.startLineNumber,
|
||||
endLineNumber: l.endLineNumber,
|
||||
} satisfies LintErrorItem))
|
||||
|
||||
if (!lintErrors.length) return { lintErrors: null }
|
||||
return { lintErrors, }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
|
||||
|
||||
@@ -52,6 +52,9 @@ import './voidSelectionHelperWidget.js'
|
||||
// register tooltip service
|
||||
import './tooltipService.js'
|
||||
|
||||
// register onboarding service
|
||||
import './voidOnboardingService.js'
|
||||
|
||||
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
|
||||
|
||||
// llmMessage
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { mountVoidOnboarding } from './react/out/void-onboarding/index.js'
|
||||
import { h, getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
|
||||
// Onboarding contribution that mounts the component at startup
|
||||
export class OnboardingContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.voidOnboarding';
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
// Get the active window reference for multi-window support
|
||||
const targetWindow = getActiveWindow();
|
||||
|
||||
// Find the monaco-workbench element using the proper window reference
|
||||
const workbench = targetWindow.document.querySelector('.monaco-workbench');
|
||||
|
||||
if (workbench) {
|
||||
|
||||
const onboardingContainer = h('div.void-onboarding-container').root;
|
||||
workbench.appendChild(onboardingContainer);
|
||||
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) {
|
||||
onboardingContainer.parentElement.removeChild(onboardingContainer);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the contribution to be initialized during the AfterRestored phase
|
||||
registerWorkbenchContribution2(OnboardingContribution.ID, OnboardingContribution, WorkbenchPhase.AfterRestored);
|
||||
@@ -8,45 +8,94 @@ import Severity from '../../../../base/common/severity.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { INotificationActions, INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
import { IUpdateService } from '../../../../platform/update/common/update.js';
|
||||
import { VoidCheckUpdateRespose } from '../common/voidUpdateServiceTypes.js';
|
||||
import { IAction } from '../../../../base/common/actions.js';
|
||||
|
||||
|
||||
|
||||
|
||||
const notifyYesUpdate = (notifService: INotificationService, res: { message?: string } = {}) => {
|
||||
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService) => {
|
||||
const message = res?.message || 'This is a very old version of Void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
|
||||
const notifController = notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: message,
|
||||
sticky: true,
|
||||
progress: { worked: 0, total: 100 },
|
||||
actions: {
|
||||
primary: [{
|
||||
id: 'void.updater.update',
|
||||
enabled: true,
|
||||
|
||||
let actions: INotificationActions | undefined
|
||||
|
||||
if (res?.action) {
|
||||
const primary: IAction[] = []
|
||||
|
||||
if (res.action === 'reinstall') {
|
||||
primary.push({
|
||||
label: `Reinstall`,
|
||||
id: 'void.updater.reinstall',
|
||||
enabled: true,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => {
|
||||
const { window } = dom.getActiveWindow()
|
||||
window.open('https://voideditor.com/download-beta')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'void.updater.site',
|
||||
})
|
||||
}
|
||||
|
||||
if (res.action === 'download') {
|
||||
primary.push({
|
||||
label: `Download`,
|
||||
id: 'void.updater.download',
|
||||
enabled: true,
|
||||
label: `Void Site`,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => {
|
||||
const { window } = dom.getActiveWindow()
|
||||
window.open('https://voideditor.com/')
|
||||
updateService.downloadUpdate()
|
||||
}
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (res.action === 'apply') {
|
||||
primary.push({
|
||||
label: `Apply`,
|
||||
id: 'void.updater.apply',
|
||||
enabled: true,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => {
|
||||
updateService.applyUpdate()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (res.action === 'restart') {
|
||||
primary.push({
|
||||
label: `Restart`,
|
||||
id: 'void.updater.restart',
|
||||
enabled: true,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => {
|
||||
updateService.quitAndInstall()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
primary.push({
|
||||
id: 'void.updater.site',
|
||||
enabled: true,
|
||||
label: `Void Site`,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => {
|
||||
const { window } = dom.getActiveWindow()
|
||||
window.open('https://voideditor.com/')
|
||||
}
|
||||
})
|
||||
|
||||
actions = {
|
||||
primary: primary,
|
||||
secondary: [{
|
||||
id: 'void.updater.close',
|
||||
enabled: true,
|
||||
@@ -57,19 +106,24 @@ const notifyYesUpdate = (notifService: INotificationService, res: { message?: st
|
||||
notifController.close()
|
||||
}
|
||||
}]
|
||||
},
|
||||
}
|
||||
}
|
||||
else {
|
||||
actions = undefined
|
||||
}
|
||||
|
||||
const notifController = notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: message,
|
||||
sticky: true,
|
||||
progress: actions ? { worked: 0, total: 100 } : undefined,
|
||||
actions: actions,
|
||||
})
|
||||
// const d = notifController.onDidClose(() => {
|
||||
// notifyYesUpdate(notifService, res)
|
||||
// d.dispose()
|
||||
// })
|
||||
}
|
||||
const notifyNoUpdate = (notifService: INotificationService) => {
|
||||
notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: 'Void is up-to-date!',
|
||||
})
|
||||
}
|
||||
const notifyErrChecking = (notifService: INotificationService) => {
|
||||
const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!`
|
||||
notifService.notify({
|
||||
@@ -80,6 +134,34 @@ const notifyErrChecking = (notifService: INotificationService) => {
|
||||
}
|
||||
|
||||
|
||||
const performVoidCheck = async (
|
||||
explicit: boolean,
|
||||
notifService: INotificationService,
|
||||
voidUpdateService: IVoidUpdateService,
|
||||
metricsService: IMetricsService,
|
||||
updateService: IUpdateService,
|
||||
) => {
|
||||
|
||||
const metricsTag = explicit ? 'Manual' : 'Auto'
|
||||
|
||||
metricsService.capture(`Void Update ${metricsTag}: Checking...`, {})
|
||||
const res = await voidUpdateService.check(explicit)
|
||||
if (!res) {
|
||||
notifyErrChecking(notifService);
|
||||
metricsService.capture(`Void Update ${metricsTag}: Error`, { res })
|
||||
}
|
||||
else {
|
||||
if (res.message) {
|
||||
notifyUpdate(res, notifService, updateService)
|
||||
metricsService.capture(`Void Update ${metricsTag}: Yes`, { res })
|
||||
}
|
||||
else {
|
||||
metricsService.capture(`Void Update ${metricsTag}: No`, { res })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Action
|
||||
registerAction2(class extends Action2 {
|
||||
@@ -94,12 +176,8 @@ registerAction2(class extends Action2 {
|
||||
const voidUpdateService = accessor.get(IVoidUpdateService)
|
||||
const notifService = accessor.get(INotificationService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
metricsService.capture('Void Update Manual: Checking...', {})
|
||||
const res = await voidUpdateService.check(true)
|
||||
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Manual: Error', { res }) }
|
||||
else if (res.hasUpdate) { notifyYesUpdate(notifService, res); metricsService.capture('Void Update Manual: Yes', { res }) }
|
||||
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update Manual: No', { res }) }
|
||||
const updateService = accessor.get(IUpdateService)
|
||||
performVoidCheck(true, notifService, voidUpdateService, metricsService, updateService)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -107,17 +185,15 @@ registerAction2(class extends Action2 {
|
||||
class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.void.voidUpdate'
|
||||
constructor(
|
||||
@IVoidUpdateService private readonly voidUpdateService: IVoidUpdateService,
|
||||
@IMetricsService private readonly metricsService: IMetricsService,
|
||||
@INotificationService private readonly notifService: INotificationService,
|
||||
@IVoidUpdateService voidUpdateService: IVoidUpdateService,
|
||||
@IMetricsService metricsService: IMetricsService,
|
||||
@INotificationService notifService: INotificationService,
|
||||
@IUpdateService updateService: IUpdateService,
|
||||
) {
|
||||
super()
|
||||
const autoCheck = async () => {
|
||||
this.metricsService.capture('Void Update Startup: Checking...', {})
|
||||
const res = await this.voidUpdateService.check(false)
|
||||
if (!res) { notifyErrChecking(this.notifService); this.metricsService.capture('Void Update Startup: Error', { res }) }
|
||||
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res); this.metricsService.capture('Void Update Startup: Yes', { res }) }
|
||||
else if (!res.hasUpdate) { this.metricsService.capture('Void Update Startup: No', { res }) } // display nothing if up to date
|
||||
|
||||
const autoCheck = () => {
|
||||
performVoidCheck(false, notifService, voidUpdateService, metricsService, updateService)
|
||||
}
|
||||
|
||||
// check once 5 seconds after mount
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
|
||||
class SurroundingsRemover {
|
||||
export class SurroundingsRemover {
|
||||
readonly originalS: string
|
||||
i: number
|
||||
j: number
|
||||
@@ -58,12 +58,13 @@ class SurroundingsRemover {
|
||||
// return offset === suffix.length
|
||||
// }
|
||||
|
||||
// either removes all or nothing
|
||||
removeFromStartUntilFullMatch = (until: string, alsoRemoveUntilStr: boolean) => {
|
||||
const index = this.originalS.indexOf(until, this.i)
|
||||
|
||||
if (index === -1) {
|
||||
this.i = this.j + 1
|
||||
return null
|
||||
// this.i = this.j + 1
|
||||
return false
|
||||
}
|
||||
// console.log('index', index, until.length)
|
||||
|
||||
|
||||
@@ -39,8 +39,11 @@ export const defaultProviderSettings = {
|
||||
apiKey: '',
|
||||
},
|
||||
xAI: {
|
||||
apiKey: ''
|
||||
apiKey: '',
|
||||
},
|
||||
mistral: {
|
||||
apiKey: '',
|
||||
}
|
||||
} as const
|
||||
|
||||
|
||||
@@ -48,11 +51,14 @@ export const defaultProviderSettings = {
|
||||
|
||||
export const defaultModelsOfProvider = {
|
||||
openAI: [ // https://platform.openai.com/docs/models/gp
|
||||
'gpt-4.1',
|
||||
'gpt-4.1-mini',
|
||||
'gpt-4.1-nano',
|
||||
'o3-mini',
|
||||
'o1',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
// 'o1',
|
||||
// 'o1-mini',
|
||||
// 'gpt-4o',
|
||||
// 'gpt-4o-mini',
|
||||
],
|
||||
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
'claude-3-7-sonnet-latest',
|
||||
@@ -78,15 +84,17 @@ export const defaultModelsOfProvider = {
|
||||
vLLM: [ // autodetected
|
||||
],
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
'anthropic/claude-3.7-sonnet:thinking',
|
||||
// 'anthropic/claude-3.7-sonnet:thinking',
|
||||
'anthropic/claude-3.7-sonnet',
|
||||
'anthropic/claude-3.5-sonnet',
|
||||
'deepseek/deepseek-r1',
|
||||
'deepseek/deepseek-r1-zero:free',
|
||||
'mistralai/codestral-2501',
|
||||
'qwen/qwen-2.5-coder-32b-instruct',
|
||||
'openrouter/quasar-alpha',
|
||||
'google/gemini-2.5-pro-preview-03-25',
|
||||
// 'mistralai/codestral-2501',
|
||||
// 'qwen/qwen-2.5-coder-32b-instruct',
|
||||
// 'mistralai/mistral-small-3.1-24b-instruct:free',
|
||||
'google/gemini-2.0-flash-lite-preview-02-05:free',
|
||||
// 'google/gemini-2.0-flash-lite-preview-02-05:free',
|
||||
// 'google/gemini-2.0-pro-exp-02-05:free',
|
||||
// 'google/gemini-2.0-flash-exp:free',
|
||||
],
|
||||
@@ -96,13 +104,12 @@ export const defaultModelsOfProvider = {
|
||||
'llama-3.1-8b-instant',
|
||||
// 'qwen-2.5-coder-32b', // preview mode (experimental)
|
||||
],
|
||||
// not supporting mistral right now- it's last on Void usage, and a huge pain to set up since it's nonstandard (it supports codestral FIM but it's on v1/fim/completions, etc)
|
||||
// mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
// 'codestral-latest',
|
||||
// 'mistral-large-latest',
|
||||
// 'ministral-3b-latest',
|
||||
// 'ministral-8b-latest',
|
||||
// ],
|
||||
mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
'codestral-latest',
|
||||
'mistral-large-latest',
|
||||
'ministral-3b-latest',
|
||||
'ministral-8b-latest',
|
||||
],
|
||||
openAICompatible: [], // fallback
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
||||
@@ -122,8 +129,8 @@ export type VoidStaticModelInfo = { // not stateful
|
||||
sizeGb: number | 'not-known'
|
||||
}
|
||||
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special parameter
|
||||
supportsTools?: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style';
|
||||
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 | {
|
||||
@@ -169,12 +176,9 @@ const modelOptionsDefaults: VoidStaticModelInfo = {
|
||||
reasoningCapabilities: false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO!!! double check all context sizes below
|
||||
// TODO!!! add openrouter common models
|
||||
// TODO!!! allow user to modify capabilities and tell them if autodetected model or falling back
|
||||
|
||||
const openSourceModelOptions_assumingOAICompat = {
|
||||
'deepseekR1': {
|
||||
supportsFIM: false,
|
||||
@@ -286,6 +290,12 @@ const openSourceModelOptions_assumingOAICompat = {
|
||||
contextWindow: 128_000, maxOutputTokens: 8_192,
|
||||
|
||||
},
|
||||
'quasar': { // openrouter/quasar-alpha
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 1_000_000, maxOutputTokens: 32_000,
|
||||
}
|
||||
} as const satisfies { [s: string]: Partial<VoidStaticModelInfo> }
|
||||
|
||||
|
||||
@@ -306,9 +316,6 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (
|
||||
...fallbackKnownValues
|
||||
}
|
||||
}
|
||||
if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower))
|
||||
return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat])
|
||||
|
||||
if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions['gemini-2.5-pro-exp-03-25'])
|
||||
|
||||
if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022'])
|
||||
@@ -339,12 +346,21 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (
|
||||
|
||||
if (lower.includes('openhands')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['openhands-lm-32b'], }) // max output unclear
|
||||
|
||||
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 (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower))
|
||||
return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat])
|
||||
|
||||
return toFallback(modelOptionsDefaults)
|
||||
}
|
||||
@@ -362,6 +378,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,
|
||||
@@ -370,6 +387,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,
|
||||
@@ -377,6 +395,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,
|
||||
},
|
||||
@@ -386,6 +405,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,
|
||||
},
|
||||
@@ -395,6 +415,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,
|
||||
},
|
||||
@@ -403,6 +424,7 @@ const anthropicModelOptions = {
|
||||
downloadable: false,
|
||||
maxOutputTokens: 4_096,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
}
|
||||
@@ -436,6 +458,36 @@ const anthropicSettings: VoidStaticProviderInfo = {
|
||||
|
||||
// ---------------- OPENAI ----------------
|
||||
const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
||||
'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,
|
||||
@@ -460,6 +512,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,
|
||||
},
|
||||
@@ -478,6 +531,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,
|
||||
},
|
||||
@@ -508,6 +562,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 = {
|
||||
@@ -615,6 +678,55 @@ const deepseekSettings: VoidStaticProviderInfo = {
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------- MISTRAL ----------------
|
||||
|
||||
const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#pricing https://docs.mistral.ai/getting-started/models/models_overview/#premier-models
|
||||
'mistral-large-latest': {
|
||||
contextWindow: 131_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 2.00, output: 6.00 },
|
||||
supportsFIM: false,
|
||||
downloadable: { sizeGb: 73 },
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'codestral-latest': {
|
||||
contextWindow: 256_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0.30, output: 0.90 },
|
||||
supportsFIM: true,
|
||||
downloadable: { sizeGb: 13 },
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'ministral-8b-latest': { // ollama 'mistral'
|
||||
contextWindow: 131_000,
|
||||
maxOutputTokens: 4_096,
|
||||
cost: { input: 0.10, output: 0.10 },
|
||||
supportsFIM: false,
|
||||
downloadable: { sizeGb: 4.1 },
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'ministral-3b-latest': {
|
||||
contextWindow: 131_000,
|
||||
maxOutputTokens: 4_096,
|
||||
cost: { input: 0.04, output: 0.04 },
|
||||
supportsFIM: false,
|
||||
downloadable: { sizeGb: 'not-known' },
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const mistralSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: mistralModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
}
|
||||
|
||||
|
||||
// ---------------- GROQ ----------------
|
||||
const groqModelOptions = { // https://console.groq.com/docs/models, https://groq.com/pricing/
|
||||
'llama-3.3-70b-versatile': {
|
||||
@@ -678,7 +790,6 @@ const ollamaModelOptions = {
|
||||
downloadable: { sizeGb: 1.9 },
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwen2.5-coder': {
|
||||
@@ -688,7 +799,6 @@ const ollamaModelOptions = {
|
||||
downloadable: { sizeGb: 4.7 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwq': {
|
||||
@@ -698,7 +808,6 @@ const ollamaModelOptions = {
|
||||
downloadable: { sizeGb: 20 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'TODO-yes-but-we-handle-it-manually',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
'deepseek-r1': {
|
||||
@@ -708,7 +817,6 @@ const ollamaModelOptions = {
|
||||
downloadable: { sizeGb: 4.7 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'TODO-yes-but-we-handle-it-manually',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
|
||||
@@ -825,7 +933,6 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0.3, output: 0.9 },
|
||||
downloadable: false,
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwen/qwen-2.5-coder-32b-instruct': {
|
||||
@@ -886,8 +993,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
||||
vLLM: vLLMSettings,
|
||||
ollama: ollamaSettings,
|
||||
openAICompatible: openaiCompatible,
|
||||
|
||||
// TODO!!!
|
||||
mistral: mistralSettings,
|
||||
// googleVertex: {},
|
||||
// microsoftAzure: {},
|
||||
// openHands: {},
|
||||
@@ -947,6 +1053,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,17 +6,22 @@
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolCallObj } from '../sendLLMMessageTypes.js';
|
||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
||||
// this is just for ease of readability
|
||||
// Triple backtick wrapper used throughout the prompts for code blocks
|
||||
export const tripleTick = ['```', '```']
|
||||
|
||||
// Maximum limits for directory structure information
|
||||
export const MAX_DIRSTR_CHARS_TOTAL_BEGINNING = 20_000
|
||||
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
|
||||
|
||||
|
||||
// Maximum character limits for prefix and suffix context
|
||||
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
|
||||
|
||||
|
||||
@@ -66,11 +71,11 @@ export const voidTools = {
|
||||
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns file contents of a given URI.`,
|
||||
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. Only fill this in if you already know the line numbers you need to search. Defaults to 1.' },
|
||||
end_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to Infinity.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
@@ -79,7 +84,7 @@ 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,
|
||||
},
|
||||
},
|
||||
@@ -101,29 +106,37 @@ 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.' },
|
||||
search_in_folder: { 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.' },
|
||||
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Returns all lint errors on a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
},
|
||||
},
|
||||
|
||||
// --- editing (create/delete) ---
|
||||
|
||||
create_file_or_folder: {
|
||||
name: 'create_file_or_folder',
|
||||
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash.`,
|
||||
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
},
|
||||
@@ -145,10 +158,9 @@ export const voidTools = {
|
||||
...uriParam('file'),
|
||||
change_description: {
|
||||
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. \
|
||||
A code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
|
||||
NEVER re-write the whole file. Bias towards writing as little as possible. \
|
||||
Here's an example of a good description:\n${editToolDescriptionExample}`
|
||||
}
|
||||
},
|
||||
@@ -158,7 +170,7 @@ Here's an example of a good description:\n${editToolDescriptionExample}`
|
||||
name: 'run_terminal_command',
|
||||
description: `Executes a terminal command.`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to execute.' },
|
||||
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.' },
|
||||
},
|
||||
@@ -205,12 +217,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 toolCallXMLStr = (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', ' ')
|
||||
}
|
||||
|
||||
@@ -218,7 +229,7 @@ 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:
|
||||
@@ -228,8 +239,8 @@ ${availableXMLToolsStr(tools)}`)
|
||||
const toolCallXMLGuidelines = (`\
|
||||
Tool calling details:
|
||||
- Once you write a tool call, you must STOP and WAIT for the result.
|
||||
- To call a tool, write its name and parameters in one of the XML formats specified above at the BOTTOM of your response.
|
||||
- All parameters are REQUIRED unless noted otherwise.
|
||||
- To call a tool, write its name and parameters in one of the XML formats specified above.
|
||||
- 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.`)
|
||||
|
||||
@@ -242,7 +253,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.`
|
||||
@@ -276,7 +287,7 @@ ${directoryStr}
|
||||
</files_overview>`)
|
||||
|
||||
|
||||
const toolDefinitions = systemToolsXMLPrompt(mode)
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
|
||||
|
||||
const details: string[] = []
|
||||
|
||||
@@ -300,8 +311,8 @@ ${directoryStr}
|
||||
}
|
||||
|
||||
if (mode === 'gather') {
|
||||
details.push(`Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.`)
|
||||
details.push(`You should extensively read files, types, content, etc and gather relevant context.`)
|
||||
details.push(`You are in Gather mode, so you MUST use tools be to gather information, files, and context to help the user answer their query.`)
|
||||
details.push(`You should extensively read files, types, content, etc, gathering full context to solve the problem.`)
|
||||
}
|
||||
|
||||
|
||||
@@ -333,7 +344,6 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
|
||||
ansStrs.push(fsInfo)
|
||||
if (toolDefinitions) ansStrs.push(toolDefinitions)
|
||||
ansStrs.push(importantDetails)
|
||||
ansStrs.push('Now, please assist the user with their query.')
|
||||
|
||||
const fullSystemMsgStr = ansStrs
|
||||
.join('\n\n\n')
|
||||
@@ -452,13 +462,13 @@ Output SEARCH/REPLACE blocks to edit the file according to the desired change. Y
|
||||
|
||||
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.
|
||||
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. The original code must NOT includes any new whitespace, comments, or any other modifications from the original code.
|
||||
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file, but please bias towards writing as little as possible.
|
||||
4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks.
|
||||
|
||||
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.
|
||||
- Make sure the "UPDATED" code is ready for production as-is, and fix any relevant lint errors.
|
||||
|
||||
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.
|
||||
|
||||
@@ -577,10 +587,16 @@ Instructions:
|
||||
`
|
||||
}
|
||||
|
||||
export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: {
|
||||
selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string,
|
||||
isOllamaFIM: false, // we require this be false for clarity
|
||||
}) => {
|
||||
export const ctrlKStream_userMessage = ({
|
||||
selection,
|
||||
prefix,
|
||||
suffix,
|
||||
instructions,
|
||||
// isOllamaFIM: false, // Remove unused variable
|
||||
fimTags,
|
||||
language }: {
|
||||
selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string,
|
||||
}) => {
|
||||
const { preTag, sufTag, midTag } = fimTags
|
||||
|
||||
// prompt the model artifically on how to do FIM
|
||||
@@ -611,39 +627,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 ========================================================
|
||||
|
||||
|
||||
@@ -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');
|
||||
@@ -98,6 +97,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 +106,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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ export type ToolCallParams = {
|
||||
'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 },
|
||||
'search_for_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
|
||||
'read_lint_errors': { uri: URI },
|
||||
// ---
|
||||
'edit_file': { uri: URI, changeDescription: string },
|
||||
'create_file_or_folder': { uri: URI, isFolder: boolean },
|
||||
@@ -38,11 +39,12 @@ export type ToolCallParams = {
|
||||
|
||||
// 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, },
|
||||
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
|
||||
'search_files': { uris: URI[], hasNextPage: boolean },
|
||||
'search_for_files': { uris: URI[], hasNextPage: boolean },
|
||||
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
|
||||
// ---
|
||||
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
|
||||
'create_file_or_folder': {},
|
||||
|
||||
@@ -71,8 +71,8 @@ export interface IVoidSettingsService {
|
||||
|
||||
|
||||
|
||||
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[] }) => {
|
||||
const { existingModels } = options
|
||||
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[], didAutoDetect: boolean }) => {
|
||||
const { existingModels, didAutoDetect } = options
|
||||
|
||||
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
|
||||
for (const existingModel of existingModels) {
|
||||
@@ -82,7 +82,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
|
||||
const newDefaultModels = defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: true,
|
||||
isAutodetected: didAutoDetect,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
}))
|
||||
|
||||
@@ -101,7 +101,30 @@ export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter:
|
||||
}
|
||||
|
||||
|
||||
const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
|
||||
const _stateWithUpdatedDefaultModels = (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 = _updatedModelsAfterDefaultModelsChange(defaultModelNames, { existingModels: currentModels, didAutoDetect: false })
|
||||
newSettingsOfProvider = {
|
||||
...newSettingsOfProvider,
|
||||
[providerName]: {
|
||||
...newSettingsOfProvider[providerName],
|
||||
models: newModels,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
}
|
||||
}
|
||||
|
||||
const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>): VoidSettingsState => {
|
||||
|
||||
let newSettingsOfProvider = state.settingsOfProvider
|
||||
|
||||
@@ -211,11 +234,21 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
}
|
||||
|
||||
async readAndInitializeState() {
|
||||
const readS = await this._readState();
|
||||
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
|
||||
}
|
||||
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);
|
||||
this.state = readS
|
||||
this.state = _stateWithUpdatedDefaultModels(this.state)
|
||||
this.state = _validatedModelState(this.state);
|
||||
|
||||
|
||||
this._resolver();
|
||||
this._onDidChangeState.fire();
|
||||
@@ -345,7 +378,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 = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models, didAutoDetect: true })
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
// if the models changed, log it
|
||||
|
||||
@@ -108,6 +108,11 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
title: 'Grok (xAI)',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return {
|
||||
title: 'Mistral API',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
@@ -134,7 +139,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
providerName === 'xAI' ? 'xai-key...' :
|
||||
'',
|
||||
providerName === 'mistral' ? 'api-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).' :
|
||||
@@ -143,8 +149,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
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 === 'openAICompatible' ? undefined :
|
||||
'',
|
||||
providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys).' :
|
||||
providerName === 'openAICompatible' ? `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).` :
|
||||
'',
|
||||
isPasswordField: true,
|
||||
}
|
||||
}
|
||||
@@ -234,31 +241,37 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.xAI),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
groq: { // aggregator
|
||||
mistral: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.mistral,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
groq: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.groq),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openRouter: { // aggregator
|
||||
openRouter: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openRouter,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openRouter),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openAICompatible: { // aggregator
|
||||
openAICompatible: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAICompatible,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAICompatible),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
ollama: { // aggregator
|
||||
ollama: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.ollama,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.ollama),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
vLLM: { // aggregator
|
||||
vLLM: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.vLLM,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||
@@ -356,6 +369,8 @@ export type GlobalSettings = {
|
||||
chatMode: ChatMode;
|
||||
autoApprove: boolean;
|
||||
showInlineSuggestions: boolean;
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
@@ -367,6 +382,8 @@ export const defaultGlobalSettings: GlobalSettings = {
|
||||
chatMode: 'agent',
|
||||
autoApprove: false,
|
||||
showInlineSuggestions: true,
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: false,
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
|
||||
@@ -7,12 +7,13 @@ import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { VoidCheckUpdateRespose } from './voidUpdateServiceTypes.js';
|
||||
|
||||
|
||||
|
||||
export interface IVoidUpdateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
check: (explicit: boolean) => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>;
|
||||
check: (explicit: boolean) => Promise<VoidCheckUpdateRespose>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
export type VoidCheckUpdateRespose = {
|
||||
message: string,
|
||||
action?: 'reinstall' | 'restart' | 'download' | 'apply'
|
||||
} | {
|
||||
message: null,
|
||||
actions?: undefined,
|
||||
} | null
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { endsWithAnyPrefixOf } from '../../common/helpers/extractCodeFromResult.js'
|
||||
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 } from '../../common/sendLLMMessageTypes.js'
|
||||
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
|
||||
import { ChatMode } from '../../common/voidSettingsTypes.js'
|
||||
import { createSaxParser } from './sax.js'
|
||||
|
||||
|
||||
// =============== reasoning ===============
|
||||
@@ -135,146 +135,148 @@ export const extractReasoningWrapper = (
|
||||
}
|
||||
|
||||
|
||||
// =============== tools ===============
|
||||
// =============== tools (XML) ===============
|
||||
|
||||
type ToolsState = {
|
||||
level: 'normal',
|
||||
} | {
|
||||
level: 'tool',
|
||||
toolName: ToolName,
|
||||
currentToolCall: RawToolCallObj,
|
||||
} | {
|
||||
level: 'param',
|
||||
toolName: ToolName,
|
||||
paramName: ToolParamName,
|
||||
currentToolCall: RawToolCallObj,
|
||||
|
||||
|
||||
const findPartiallyWrittenToolTagAtEnd = (fullText: string, toolTags: string[]) => {
|
||||
for (const toolTag of toolTags) {
|
||||
const foundPrefix = endsWithAnyPrefixOf(fullText, toolTag)
|
||||
if (foundPrefix) {
|
||||
return [foundPrefix, toolTag] as const
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const extractToolsWrapper = (
|
||||
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode
|
||||
const findIndexOfAny = (fullText: string, matches: string[]) => {
|
||||
for (const str of matches) {
|
||||
const idx = fullText.indexOf(str);
|
||||
if (idx !== -1) {
|
||||
return [idx, str] as const
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
|
||||
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const paramsObj: RawToolParamsObj = {}
|
||||
const doneParams: ToolParamName[] = []
|
||||
let isDone = false
|
||||
|
||||
const getAnswer = (): RawToolCallObj => {
|
||||
// trim off all whitespace at and before first \n and after last \n for each param
|
||||
for (const p in paramsObj) {
|
||||
const paramName = p as ToolParamName
|
||||
const orig = paramsObj[paramName]
|
||||
if (orig === undefined) continue
|
||||
paramsObj[paramName] = trimBeforeAndAfterNewLines(orig)
|
||||
}
|
||||
|
||||
// return tool call
|
||||
const ans: RawToolCallObj = {
|
||||
name: toolName,
|
||||
rawParams: paramsObj,
|
||||
doneParams: doneParams,
|
||||
isDone: isDone,
|
||||
id: toolId,
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
// find first toolName tag
|
||||
const openToolTag = `<${toolName}>`
|
||||
let i = str.indexOf(openToolTag)
|
||||
if (i === -1) return getAnswer()
|
||||
let j = str.lastIndexOf(`</${toolName}>`)
|
||||
if (j === -1) j = Infinity
|
||||
else isDone = true
|
||||
|
||||
|
||||
str = str.substring(i + openToolTag.length, j)
|
||||
|
||||
const pm = new SurroundingsRemover(str)
|
||||
|
||||
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[]
|
||||
if (allowedParams.length === 0) return getAnswer()
|
||||
let latestMatchedOpenParam: null | ToolParamName = null
|
||||
let n = 0
|
||||
while (true) {
|
||||
n += 1
|
||||
if (n > 10) return getAnswer() // just for good measure as this code is early
|
||||
|
||||
// find the param name opening tag
|
||||
let matchedOpenParam: null | ToolParamName = null
|
||||
for (const paramName of allowedParams) {
|
||||
const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true)
|
||||
if (removed) {
|
||||
matchedOpenParam = paramName
|
||||
break
|
||||
}
|
||||
}
|
||||
// if did not find a new param, stop
|
||||
if (matchedOpenParam === null) {
|
||||
if (latestMatchedOpenParam !== null) {
|
||||
paramsObj[latestMatchedOpenParam] += pm.value()
|
||||
}
|
||||
return getAnswer()
|
||||
}
|
||||
else {
|
||||
latestMatchedOpenParam = matchedOpenParam
|
||||
}
|
||||
|
||||
paramsObj[latestMatchedOpenParam] = ''
|
||||
|
||||
// find the param name closing tag
|
||||
let matchedCloseParam: boolean = false
|
||||
let paramContents = ''
|
||||
for (const paramName of allowedParams) {
|
||||
const i = pm.i
|
||||
const closeTag = `</${paramName}>`
|
||||
const removed = pm.removeFromStartUntilFullMatch(closeTag, true)
|
||||
if (removed) {
|
||||
const i2 = pm.i
|
||||
paramContents = pm.originalS.substring(i, i2 - closeTag.length)
|
||||
matchedCloseParam = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// if did not find a new close tag, stop
|
||||
if (!matchedCloseParam) {
|
||||
paramsObj[latestMatchedOpenParam] += pm.value()
|
||||
return getAnswer()
|
||||
}
|
||||
else {
|
||||
doneParams.push(latestMatchedOpenParam)
|
||||
}
|
||||
|
||||
paramsObj[latestMatchedOpenParam] += paramContents
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
const toolOfToolName: { [toolName: string]: InternalToolInfo | undefined } = {}
|
||||
const toolOfToolName: ToolOfToolName = {}
|
||||
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 = ''
|
||||
const firstToolCallRef: { current: RawToolCallObj | undefined } = { current: undefined }
|
||||
|
||||
let state: ToolsState = { level: 'normal' }
|
||||
|
||||
|
||||
const getRawNewText = () => {
|
||||
return trueFullText.substring(parser.startTagPosition, parser.position + 1)
|
||||
}
|
||||
const parser = createSaxParser()
|
||||
|
||||
// when see open tag <tagName>
|
||||
parser.onopentag = (node) => {
|
||||
const rawNewText = getRawNewText()
|
||||
const tagName = node.name;
|
||||
console.log('OPENING', tagName)
|
||||
console.log('state0:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
|
||||
|
||||
if (state.level === 'normal') {
|
||||
if (tagName in toolOfToolName) { // valid toolName
|
||||
state = {
|
||||
level: 'tool',
|
||||
toolName: tagName as ToolName,
|
||||
currentToolCall: { name: tagName as ToolName, rawParams: {}, doneParams: [], isDone: false }
|
||||
}
|
||||
firstToolCallRef.current = state.currentToolCall
|
||||
}
|
||||
else {
|
||||
fullText += rawNewText // count as plaintext
|
||||
console.log('adding raw a', rawNewText)
|
||||
|
||||
}
|
||||
}
|
||||
else if (state.level === 'tool') {
|
||||
if (tagName in (toolOfToolName[state.toolName]?.params ?? {})) { // valid param
|
||||
state = {
|
||||
level: 'param',
|
||||
toolName: state.toolName,
|
||||
paramName: tagName as ToolParamName,
|
||||
currentToolCall: state.currentToolCall,
|
||||
}
|
||||
}
|
||||
else {
|
||||
// would normally be rawNewText, but we ignore all text inside tools
|
||||
}
|
||||
}
|
||||
else if (state.level === 'param') { // cannot double nest
|
||||
fullText += rawNewText // count as plaintext
|
||||
console.log('adding raw b', rawNewText)
|
||||
|
||||
}
|
||||
|
||||
console.log('state1:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
|
||||
|
||||
};
|
||||
|
||||
parser.onclosetag = (tagName) => {
|
||||
const rawNewText = getRawNewText()
|
||||
console.log('CLOSING', tagName)
|
||||
console.log('state0:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
|
||||
|
||||
|
||||
if (state.level === 'normal') {
|
||||
fullText += rawNewText
|
||||
console.log('adding raw A', rawNewText)
|
||||
}
|
||||
else if (state.level === 'tool') {
|
||||
if (tagName === state.toolName) { // closed the tool
|
||||
state.currentToolCall.isDone = true
|
||||
state = {
|
||||
level: 'normal',
|
||||
}
|
||||
}
|
||||
else { // add as text
|
||||
fullText += rawNewText
|
||||
console.log('adding raw B', rawNewText)
|
||||
}
|
||||
}
|
||||
else if (state.level === 'param') {
|
||||
if (tagName === state.paramName) { // closed the param
|
||||
state.currentToolCall.doneParams.push(state.paramName)
|
||||
state = {
|
||||
level: 'tool',
|
||||
toolName: state.toolName,
|
||||
currentToolCall: state.currentToolCall,
|
||||
}
|
||||
}
|
||||
else {
|
||||
fullText += rawNewText
|
||||
console.log('adding raw C', rawNewText)
|
||||
|
||||
}
|
||||
}
|
||||
console.log('state1:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
parser.ontext = (text) => {
|
||||
if (state.level === 'normal') {
|
||||
fullText += text
|
||||
}
|
||||
// start param
|
||||
else if (state.level === 'tool') {
|
||||
// ignore all text in a tool, all text should go in the param tags inside it
|
||||
}
|
||||
else if (state.level === 'param') {
|
||||
if (!(state.paramName in state.currentToolCall.rawParams)) state.currentToolCall.rawParams[state.paramName] = ''
|
||||
state.currentToolCall.rawParams[state.paramName] += text
|
||||
}
|
||||
}
|
||||
|
||||
let latestToolCall: RawToolCallObj | undefined = undefined
|
||||
|
||||
let foundOpenTag: { idx: number, toolName: ToolName } | null = null
|
||||
let openToolTagBuffer = '' // the characters we've seen so far that come after a < with no space afterwards, not yet added to fullText
|
||||
|
||||
let prevFullTextLen = 0
|
||||
const newOnText: OnText = (params) => {
|
||||
@@ -282,13 +284,53 @@ export const extractToolsWrapper = (
|
||||
prevFullTextLen = params.fullText.length
|
||||
trueFullText = params.fullText
|
||||
|
||||
parser.write(newText)
|
||||
// console.log('NEWTEXT', JSON.stringify(newText))
|
||||
|
||||
|
||||
if (foundOpenTag === null) {
|
||||
const newFullText = openToolTagBuffer + newText
|
||||
// ensure the code below doesn't run if only half a tag has been written
|
||||
const isPartial = findPartiallyWrittenToolTagAtEnd(newFullText, toolOpenTags)
|
||||
if (isPartial) {
|
||||
// console.log('--- partial!!!')
|
||||
openToolTagBuffer += newText
|
||||
}
|
||||
// if no tooltag is partially written at the end, attempt to get the index
|
||||
else {
|
||||
// we will instantly retroactively remove this if it's a tag match
|
||||
fullText += openToolTagBuffer
|
||||
openToolTagBuffer = ''
|
||||
fullText += newText
|
||||
|
||||
const i = findIndexOfAny(fullText, toolOpenTags)
|
||||
if (i !== null) {
|
||||
const [idx, toolTag] = i
|
||||
const toolName = toolTag.substring(1, toolTag.length - 1) as ToolName
|
||||
// console.log('found ', toolName)
|
||||
foundOpenTag = { idx, toolName }
|
||||
|
||||
// do not count anything at or after i in fullText
|
||||
fullText = fullText.substring(0, idx)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// toolTagIdx is not null, so parse the XML
|
||||
if (foundOpenTag !== null) {
|
||||
latestToolCall = parseXMLPrefixToToolCall(
|
||||
foundOpenTag.toolName,
|
||||
toolId,
|
||||
trueFullText.substring(foundOpenTag.idx, Infinity),
|
||||
toolOfToolName,
|
||||
)
|
||||
}
|
||||
|
||||
// firstToolCallRef.current === state.currentToolCall is always true
|
||||
onText({
|
||||
...params,
|
||||
fullText,
|
||||
toolCall: firstToolCallRef.current,
|
||||
toolCall: latestToolCall,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -298,16 +340,7 @@ export const extractToolsWrapper = (
|
||||
newOnText({ ...params })
|
||||
|
||||
fullText = fullText.trimEnd()
|
||||
const toolCall = firstToolCallRef.current
|
||||
if (toolCall) {
|
||||
// trim off all whitespace at and before first \n and after last \n for each param
|
||||
for (const p in toolCall.rawParams) {
|
||||
const paramName = p as ToolParamName
|
||||
const orig = toolCall.rawParams[paramName]
|
||||
if (orig === undefined) continue
|
||||
toolCall.rawParams[paramName] = trimBeforeAndAfterNewLines(orig)
|
||||
}
|
||||
}
|
||||
const toolCall = latestToolCall
|
||||
|
||||
// console.log('final message!!!', trueFullText)
|
||||
// console.log('----- returning ----\n', fullText)
|
||||
@@ -321,7 +354,7 @@ export const extractToolsWrapper = (
|
||||
|
||||
|
||||
|
||||
// trim all whitespace up until the first newline, and all whitespace after the last newline
|
||||
// trim all whitespace up until the first newline, and all whitespace up until the last newline
|
||||
const trimBeforeAndAfterNewLines = (s: string) => {
|
||||
if (!s) return s;
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// Define options for the parser.
|
||||
export interface SaxParserOptions {
|
||||
lowercase?: boolean;
|
||||
}
|
||||
|
||||
// Define the structure for a parsed node.
|
||||
export interface SaxNode {
|
||||
name: string;
|
||||
attributes: { [key: string]: string };
|
||||
}
|
||||
|
||||
// Define the interface for the SAX-like parser.
|
||||
export interface SaxParser {
|
||||
// Event handlers that can be set by the consumer.
|
||||
onopentag: ((node: SaxNode) => void) | null;
|
||||
ontext: ((text: string) => void) | null;
|
||||
onclosetag: ((tagName: string) => void) | null;
|
||||
// Properties to track current positions (used for raw text extraction).
|
||||
startTagPosition: number;
|
||||
position: number;
|
||||
// Processes a new chunk of text.
|
||||
write(chunk: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal, event-driven SAX-like parser.
|
||||
*
|
||||
* @param options An object of type `SaxParserOptions`. Passing `{ lowercase: true }` will force all tag names to be lower-cased.
|
||||
* @returns A parser object implementing the `SaxParser` interface.
|
||||
*/
|
||||
export function createSaxParser(options: SaxParserOptions = {}): SaxParser {
|
||||
// Buffer to hold any leftover text (part of an incomplete tag).
|
||||
let buffer: string = '';
|
||||
// Global counter to track the total processed characters.
|
||||
let globalPos: number = 0;
|
||||
|
||||
const parser: SaxParser = {
|
||||
onopentag: null,
|
||||
ontext: null,
|
||||
onclosetag: null,
|
||||
startTagPosition: 0,
|
||||
position: 0,
|
||||
|
||||
write(chunk: string): void {
|
||||
// Set the starting position before processing the new chunk.
|
||||
this.startTagPosition = globalPos;
|
||||
buffer += chunk;
|
||||
globalPos += chunk.length;
|
||||
// Set the current position to the end of the processed chunk.
|
||||
this.position = globalPos - 1;
|
||||
|
||||
let cursor = 0;
|
||||
// Flag to indicate if an incomplete tag was found.
|
||||
let incompleteTagFound = false;
|
||||
// This will mark the position in the buffer where the incomplete tag starts.
|
||||
let incompleteStart = 0;
|
||||
|
||||
while (cursor < buffer.length) {
|
||||
// Look for the next opening '<' character.
|
||||
const ltIndex = buffer.indexOf('<', cursor);
|
||||
if (ltIndex === -1) {
|
||||
// No more tags found in the current buffer.
|
||||
if (cursor < buffer.length && this.ontext) {
|
||||
this.ontext(buffer.substring(cursor));
|
||||
}
|
||||
// All content is processed.
|
||||
buffer = '';
|
||||
cursor = buffer.length;
|
||||
break;
|
||||
}
|
||||
|
||||
// Emit any text between the current cursor and the opening tag.
|
||||
if (ltIndex > cursor && this.ontext) {
|
||||
this.ontext(buffer.substring(cursor, ltIndex));
|
||||
}
|
||||
|
||||
// Look for the closing '>' character starting from the found '<'.
|
||||
const gtIndex = buffer.indexOf('>', ltIndex);
|
||||
if (gtIndex === -1) {
|
||||
// Incomplete tag detected.
|
||||
incompleteTagFound = true;
|
||||
// Save the starting point of the incomplete tag.
|
||||
incompleteStart = ltIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract the tag content (excluding the '<' and '>').
|
||||
let tagContent = buffer.substring(ltIndex + 1, gtIndex).trim();
|
||||
if (!tagContent) {
|
||||
cursor = gtIndex + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a closing tag (starts with '/').
|
||||
if (tagContent[0] === '/') {
|
||||
let tagName = tagContent.substring(1).trim();
|
||||
if (options.lowercase && tagName) {
|
||||
tagName = tagName.toLowerCase();
|
||||
}
|
||||
if (this.onclosetag) {
|
||||
this.onclosetag(tagName);
|
||||
}
|
||||
} else {
|
||||
// Handle self-closing tags (ending with '/').
|
||||
let selfClosing = false;
|
||||
if (tagContent[tagContent.length - 1] === '/') {
|
||||
selfClosing = true;
|
||||
tagContent = tagContent.slice(0, -1).trim();
|
||||
}
|
||||
// Determine the tag name (first word before any whitespace).
|
||||
const spaceIndex = tagContent.indexOf(' ');
|
||||
let tagName =
|
||||
spaceIndex !== -1
|
||||
? tagContent.substring(0, spaceIndex).trim()
|
||||
: tagContent;
|
||||
if (options.lowercase && tagName) {
|
||||
tagName = tagName.toLowerCase();
|
||||
}
|
||||
// Emit an open tag event.
|
||||
if (this.onopentag) {
|
||||
const node: SaxNode = { name: tagName, attributes: {} };
|
||||
this.onopentag(node);
|
||||
}
|
||||
// If it’s a self-closing tag, immediately emit a close tag event.
|
||||
if (selfClosing && this.onclosetag) {
|
||||
this.onclosetag(tagName);
|
||||
}
|
||||
}
|
||||
// Move the cursor past the current tag.
|
||||
cursor = gtIndex + 1;
|
||||
}
|
||||
|
||||
// If an incomplete tag was detected, preserve it.
|
||||
if (incompleteTagFound) {
|
||||
// Keep the incomplete portion starting from the '<'
|
||||
buffer = buffer.substring(incompleteStart);
|
||||
} else {
|
||||
// Otherwise, remove all processed content.
|
||||
buffer = buffer.substring(cursor);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return parser;
|
||||
}
|
||||
@@ -6,16 +6,18 @@
|
||||
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 { 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;
|
||||
@@ -26,8 +28,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>
|
||||
|
||||
|
||||
@@ -84,12 +86,16 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
|
||||
else throw new Error(`Void providerName was invalid: ${providerName}.`)
|
||||
}
|
||||
|
||||
|
||||
const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => {
|
||||
const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
|
||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
@@ -99,16 +105,14 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
|
||||
return
|
||||
}
|
||||
|
||||
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
|
||||
|
||||
const openai = 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
|
||||
@@ -121,14 +125,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 = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
supportsSystemMessage,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
specialToolFormat,
|
||||
reasoningCapabilities,
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
|
||||
@@ -139,16 +190,17 @@ 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 ? { 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 options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
messages: messages as any,
|
||||
stream: true,
|
||||
...nativeToolsObj,
|
||||
// max_completion_tokens: maxTokens,
|
||||
}
|
||||
|
||||
@@ -161,9 +213,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
|
||||
}
|
||||
@@ -171,6 +223,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 => {
|
||||
@@ -181,6 +237,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) {
|
||||
@@ -189,14 +256,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)
|
||||
@@ -244,14 +318,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 = ({ 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
|
||||
@@ -262,26 +370,32 @@ 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 ? { 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
|
||||
}
|
||||
@@ -290,9 +404,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
|
||||
@@ -300,46 +422,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) => {
|
||||
@@ -349,6 +474,43 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
||||
_setAborter(() => stream.controller.abort())
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ------------ MISTRAL ------------
|
||||
// https://docs.mistral.ai/api/#tag/fim
|
||||
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
|
||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
|
||||
else
|
||||
onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null })
|
||||
return
|
||||
}
|
||||
|
||||
const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey })
|
||||
fimComplete(mistral,
|
||||
{
|
||||
model: modelName,
|
||||
prompt: messages.prefix,
|
||||
suffix: messages.suffix,
|
||||
stream: false,
|
||||
maxTokens: 300,
|
||||
stop: messages.stopTokens,
|
||||
})
|
||||
.then(async response => {
|
||||
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('')
|
||||
|
||||
onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ------------ OLLAMA ------------
|
||||
const newOllamaSDK = ({ endpoint }: { endpoint: string }) => {
|
||||
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
@@ -381,12 +543,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,
|
||||
@@ -394,7 +554,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,
|
||||
@@ -445,11 +605,11 @@ export const sendLLMMessageToProviderImplementation = {
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
// mistral: {
|
||||
// sendChat: , // TODO
|
||||
// sendFIM: , // TODO // https://docs.mistral.ai/api/#tag/fim
|
||||
// list: null,
|
||||
// },
|
||||
mistral: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: (params) => sendMistralFIM(params),
|
||||
list: null,
|
||||
},
|
||||
ollama: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: sendOllamaFIM,
|
||||
|
||||
@@ -11,7 +11,6 @@ import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
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 })
|
||||
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 })
|
||||
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,6 +8,7 @@ import { IEnvironmentMainService } from '../../../../platform/environment/electr
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { IUpdateService, StateType } from '../../../../platform/update/common/update.js';
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
import { VoidCheckUpdateRespose } from '../common/voidUpdateServiceTypes.js';
|
||||
|
||||
|
||||
|
||||
@@ -17,48 +18,86 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
||||
constructor(
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
|
||||
@IUpdateService private readonly _updateService: IUpdateService
|
||||
@IUpdateService private readonly _updateService: IUpdateService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
nIgnores = 0
|
||||
async check(explicit: boolean) {
|
||||
async check(explicit: boolean): Promise<VoidCheckUpdateRespose> {
|
||||
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
|
||||
if (isDevMode) {
|
||||
return { hasUpdate: false } as const
|
||||
return { message: null } as const
|
||||
}
|
||||
|
||||
this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves
|
||||
|
||||
console.log('updateState', this._updateService.state)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.Idle) {
|
||||
// No updates currently available
|
||||
return { message: explicit ? 'No update found!' : null, action: explicit ? 'reinstall' : undefined } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.CheckingForUpdates) {
|
||||
// Currently checking for updates
|
||||
return { message: explicit ? 'No updates found!' : null } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.AvailableForDownload) {
|
||||
// Update available but requires manual download (mainly for Linux)
|
||||
return { message: 'A new update is available!', action: 'download', } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.Downloading) {
|
||||
// Update is currently being downloaded
|
||||
return { message: explicit ? 'Currently downloading update...' : null } as const
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.Updating) {
|
||||
// Update is being applied
|
||||
return { message: explicit ? 'Applying update...' : null } as const
|
||||
}
|
||||
|
||||
if (this._updateService.state.type === StateType.Ready) {
|
||||
return { hasUpdate: true, message: 'Restart Void to update!' }
|
||||
// Update is ready
|
||||
return { message: 'Restart Void to update!', action: 'restart' } as const
|
||||
}
|
||||
|
||||
const wasAutomaticCheck = !explicit // ignore the first auto check, just use it to call updateService.check()
|
||||
if (wasAutomaticCheck && this.nIgnores < 1) {
|
||||
this.nIgnores += 1
|
||||
return { hasUpdate: false } as const
|
||||
}
|
||||
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()
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
|
||||
const resJSON = await res.json()
|
||||
if (!resJSON) return null // null means error
|
||||
|
||||
if (!resJSON) return null // null means error
|
||||
const { hasUpdate, downloadMessage } = resJSON ?? {}
|
||||
if (hasUpdate === undefined)
|
||||
return null
|
||||
|
||||
const { hasUpdate, downloadMessage } = resJSON ?? {}
|
||||
if (hasUpdate === undefined)
|
||||
const after = (downloadMessage || '') + ''
|
||||
if (hasUpdate)
|
||||
return { message: after, action: 'reinstall' } as const
|
||||
return { message: 'Void is up-to-date!' } as const
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
const after = (downloadMessage || '') + ''
|
||||
return { hasUpdate: !!hasUpdate, message: after }
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user