mirror of
https://github.com/voideditor/void
synced 2026-05-24 01:48:25 +00:00
Merge branch 'main' into pr/jcommaret/321
This commit is contained in:
commit
22f4a75c00
63 changed files with 4907 additions and 1953 deletions
|
|
@ -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 [Contributing](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.
|
||||
|
|
|
|||
162
VOID_CODEBASE_GUIDE.md
Normal file
162
VOID_CODEBASE_GUIDE.md
Normal file
|
|
@ -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).
|
||||
|
||||
|
||||
|
|
@ -24,12 +24,12 @@ function makeCompileBuildTask(disableMangle) {
|
|||
);
|
||||
}
|
||||
|
||||
// Local/PR compile, including nls and inline sources in sourcemaps, minification, no mangling
|
||||
const compileBuildWithoutManglingTask = task.define('compile-build-without-mangling', makeCompileBuildTask(true));
|
||||
gulp.task(compileBuildWithoutManglingTask);
|
||||
exports.compileBuildWithoutManglingTask = compileBuildWithoutManglingTask;
|
||||
// Full compile, including nls and inline sources in sourcemaps, mangling, minification, for build
|
||||
const compileBuildTask = task.define('compile-build', makeCompileBuildTask(false));
|
||||
gulp.task(compileBuildTask);
|
||||
exports.compileBuildTask = compileBuildTask;
|
||||
|
||||
// CI compile, including nls and inline sources in sourcemaps, mangling, minification, for build
|
||||
const compileBuildWithManglingTask = task.define('compile-build-with-mangling', makeCompileBuildTask(false));
|
||||
gulp.task(compileBuildWithManglingTask);
|
||||
exports.compileBuildWithManglingTask = compileBuildWithManglingTask;
|
||||
// Full compile for PR ci, e.g no mangling
|
||||
const compileBuildTaskPullRequest = task.define('compile-build-pr', makeCompileBuildTask(true));
|
||||
gulp.task(compileBuildTaskPullRequest);
|
||||
exports.compileBuildTaskPullRequest = compileBuildTaskPullRequest;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const gunzip = require('gulp-gunzip');
|
|||
const File = require('vinyl');
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
const { compileBuildWithManglingTask } = require('./gulpfile.compile');
|
||||
const { compileBuildTask } = require('./gulpfile.compile');
|
||||
const { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileExtensionMediaBuildTask } = require('./gulpfile.extensions');
|
||||
const { vscodeWebResourceIncludes, createVSCodeWebFileContentMapper } = require('./gulpfile.vscode.web');
|
||||
const cp = require('child_process');
|
||||
|
|
@ -491,7 +491,7 @@ function tweakProductForServerWeb(product) {
|
|||
gulp.task(serverTaskCI);
|
||||
|
||||
const serverTask = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series(
|
||||
compileBuildWithManglingTask,
|
||||
compileBuildTask,
|
||||
cleanExtensionsBuildTask,
|
||||
compileNonNativeExtensionsBuildTask,
|
||||
compileExtensionMediaBuildTask,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const { getProductionDependencies } = require('./lib/dependencies');
|
|||
const { config } = require('./lib/electron');
|
||||
const createAsar = require('./lib/asar').createAsar;
|
||||
const minimist = require('minimist');
|
||||
const { compileBuildWithoutManglingTask, compileBuildWithManglingTask } = require('./gulpfile.compile');
|
||||
const { compileBuildTask } = require('./gulpfile.compile');
|
||||
const { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } = require('./gulpfile.extensions');
|
||||
const { promisify } = require('util');
|
||||
const glob = promisify(require('glob'));
|
||||
|
|
@ -166,25 +166,25 @@ const minifyVSCodeTask = task.define('minify-vscode', task.series(
|
|||
));
|
||||
gulp.task(minifyVSCodeTask);
|
||||
|
||||
const coreCI = task.define('core-ci', task.series(
|
||||
gulp.task('compile-build-with-mangling'),
|
||||
const core = task.define('core-ci', task.series(
|
||||
gulp.task('compile-build'),
|
||||
task.parallel(
|
||||
gulp.task('minify-vscode'),
|
||||
gulp.task('minify-vscode-reh'),
|
||||
gulp.task('minify-vscode-reh-web'),
|
||||
)
|
||||
));
|
||||
gulp.task(coreCI);
|
||||
gulp.task(core);
|
||||
|
||||
const coreCIPR = task.define('core-ci-pr', task.series(
|
||||
gulp.task('compile-build-without-mangling'),
|
||||
const corePr = task.define('core-ci-pr', task.series(
|
||||
gulp.task('compile-build-pr'),
|
||||
task.parallel(
|
||||
gulp.task('minify-vscode'),
|
||||
gulp.task('minify-vscode-reh'),
|
||||
gulp.task('minify-vscode-reh-web'),
|
||||
)
|
||||
));
|
||||
gulp.task(coreCIPR);
|
||||
gulp.task(corePr);
|
||||
|
||||
/**
|
||||
* Compute checksums for some files.
|
||||
|
|
@ -502,7 +502,7 @@ BUILD_TARGETS.forEach(buildTarget => {
|
|||
gulp.task(vscodeTaskCI);
|
||||
|
||||
const vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series(
|
||||
minified ? compileBuildWithManglingTask : compileBuildWithoutManglingTask,
|
||||
compileBuildTask,
|
||||
cleanExtensionsBuildTask,
|
||||
compileNonNativeExtensionsBuildTask,
|
||||
compileExtensionMediaBuildTask,
|
||||
|
|
@ -540,7 +540,7 @@ const innoSetupConfig = {
|
|||
gulp.task(task.define(
|
||||
'vscode-translations-export',
|
||||
task.series(
|
||||
coreCI,
|
||||
core,
|
||||
compileAllExtensionsBuildTask,
|
||||
function () {
|
||||
const pathToMetadata = './out-build/nls.metadata.json';
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const filter = require('gulp-filter');
|
|||
const { getProductionDependencies } = require('./lib/dependencies');
|
||||
const vfs = require('vinyl-fs');
|
||||
const packageJson = require('../package.json');
|
||||
const { compileBuildWithManglingTask } = require('./gulpfile.compile');
|
||||
const { compileBuildTask } = require('./gulpfile.compile');
|
||||
const extensions = require('./lib/extensions');
|
||||
const VinylFile = require('vinyl');
|
||||
|
||||
|
|
@ -223,7 +223,7 @@ const dashed = (/** @type {string} */ str) => (str ? `-${str}` : ``);
|
|||
gulp.task(vscodeWebTaskCI);
|
||||
|
||||
const vscodeWebTask = task.define(`vscode-web${dashed(minified)}`, task.series(
|
||||
compileBuildWithManglingTask,
|
||||
compileBuildTask,
|
||||
vscodeWebTaskCI
|
||||
));
|
||||
gulp.task(vscodeWebTask);
|
||||
|
|
|
|||
37
package-lock.json
generated
37
package-lock.json
generated
|
|
@ -62,6 +62,7 @@
|
|||
"posthog-node": "^4.8.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-html-languageservice": "^5.3.1",
|
||||
|
|
@ -6621,6 +6622,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
||||
|
|
@ -15094,10 +15101,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
|
||||
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
|
|
@ -17999,6 +18007,20 @@
|
|||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-tooltip": {
|
||||
"version": "5.28.1",
|
||||
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.1.tgz",
|
||||
"integrity": "sha512-ZA4oHwoIIK09TS7PvSLFcRlje1wGZaxw6xHvfrzn6T82UcMEfEmHVCad16Gnr4NDNDh93HyN037VK4HDi5odfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.1",
|
||||
"classnames": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
|
@ -18807,10 +18829,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
|
||||
"dev": true
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.25.0",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
"eslint": "node build/eslint",
|
||||
"stylelint": "node build/stylelint",
|
||||
"playwright-install": "npm exec playwright install",
|
||||
"compile-build": "node ./node_modules/gulp/bin/gulp.js compile-build-with-mangling",
|
||||
"compile-build": "node ./node_modules/gulp/bin/gulp.js compile-build",
|
||||
"compile-extensions-build": "node ./node_modules/gulp/bin/gulp.js compile-extensions-build",
|
||||
"minify-vscode": "node ./node_modules/gulp/bin/gulp.js minify-vscode",
|
||||
"minify-vscode-reh": "node ./node_modules/gulp/bin/gulp.js minify-vscode-reh",
|
||||
|
|
@ -123,6 +123,7 @@
|
|||
"posthog-node": "^4.8.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-html-languageservice": "^5.3.1",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// This bootstrap-fork module handles the initialization of a forked process in VS Code.
|
||||
// It sets up logging, exception handling, and loads the ESM module system.
|
||||
|
||||
import * as performance from './vs/base/common/performance.js';
|
||||
import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from './bootstrap-node.js';
|
||||
import { bootstrapESM } from './bootstrap-esm.js';
|
||||
|
|
|
|||
|
|
@ -121,6 +121,93 @@ export class SmartSelectController implements IEditorContribution {
|
|||
}
|
||||
this._state = this._state.map(state => state.mov(forward));
|
||||
const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition()));
|
||||
|
||||
// Void changed this to skip over added whitespace when using smartSelect
|
||||
// // Store the original selections for comparison
|
||||
// const originalSelections = selections;
|
||||
|
||||
// // Keep skipping while we're only adding/removing whitespace
|
||||
// let keepSkipping = true;
|
||||
// let skipCount = 0;
|
||||
// const MAX_SKIPS = 5; // Avoid infinite loops by setting a reasonable limit
|
||||
|
||||
// while (keepSkipping && skipCount < MAX_SKIPS) {
|
||||
// keepSkipping = false; // Reset for each iteration
|
||||
|
||||
// // Check if all selections only added/removed whitespace
|
||||
// if (originalSelections.length === newSelections.length) {
|
||||
// for (let i = 0; i < originalSelections.length; i++) {
|
||||
// const oldSel = originalSelections[i];
|
||||
// const newSel = newSelections[i];
|
||||
|
||||
// if (forward) { // For expanding (^+Shift+Right)
|
||||
// // Skip if only whitespace was added
|
||||
// const oldText = model.getValueInRange(oldSel).trim();
|
||||
// const newText = model.getValueInRange(newSel).trim();
|
||||
// const onlyWhitespaceAdded = oldText === newText && oldText.length > 0;
|
||||
|
||||
// if (onlyWhitespaceAdded) {
|
||||
// console.log(`SMART SELECT - SKIPPING (EXPAND) [${skipCount + 1}]:`, {
|
||||
// reason: 'only whitespace added',
|
||||
// oldText: model.getValueInRange(oldSel),
|
||||
// newText: model.getValueInRange(newSel)
|
||||
// });
|
||||
// keepSkipping = true;
|
||||
// break;
|
||||
// }
|
||||
// } else { // For shrinking (^+Shift+Left)
|
||||
// // Skip if only whitespace was removed
|
||||
// const oldText = model.getValueInRange(oldSel).trim();
|
||||
// const newText = model.getValueInRange(newSel).trim();
|
||||
// const onlyWhitespaceRemoved = oldText === newText && newText.length > 0;
|
||||
|
||||
// if (onlyWhitespaceRemoved) {
|
||||
// console.log(`SMART SELECT - SKIPPING (SHRINK) [${skipCount + 1}]:`, {
|
||||
// reason: 'only whitespace removed',
|
||||
// oldText: model.getValueInRange(oldSel),
|
||||
// newText: model.getValueInRange(newSel)
|
||||
// });
|
||||
// keepSkipping = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // If we need to skip, move one more time
|
||||
// if (keepSkipping) {
|
||||
// skipCount++;
|
||||
|
||||
// // Try to move to the next range
|
||||
// const prevState = this._state;
|
||||
// this._state = this._state.map(state => state.mov(forward));
|
||||
|
||||
// // Check if we've reached the end of available ranges
|
||||
// const stateUnchanged = this._state.every((state, idx) =>
|
||||
// state.index === prevState[idx].index
|
||||
// );
|
||||
|
||||
// if (stateUnchanged) {
|
||||
// // We can't move any further, so stop skipping
|
||||
// keepSkipping = false;
|
||||
// } else {
|
||||
// // Update selections for the next iteration
|
||||
// newSelections = this._state.map(state => Selection.fromPositions(
|
||||
// state.ranges[state.index].getStartPosition(),
|
||||
// state.ranges[state.index].getEndPosition()
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Print AFTER selection (before actually setting it)
|
||||
// console.log('SMART SELECT - AFTER:', newSelections.map(s => {
|
||||
// return {
|
||||
// range: `(${s.startLineNumber},${s.startColumn}) -> (${s.endLineNumber},${s.endColumn})`,
|
||||
// text: model.getValueInRange(s)
|
||||
// };
|
||||
// }));
|
||||
|
||||
this._ignoreSelection = true;
|
||||
try {
|
||||
this._editor.setSelections(newSelections);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -612,7 +612,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
|||
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
|
||||
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
|
||||
// On Mac, the delay is 1500.
|
||||
'default': isMacintosh ? 1500 : 500,
|
||||
'default': 300, // Void changed this from isMacintosh ? 1500 : 500,
|
||||
'minimum': 0
|
||||
},
|
||||
'workbench.reduceMotion': {
|
||||
|
|
|
|||
|
|
@ -648,15 +648,18 @@ const defaultChat = {
|
|||
providerSetting: product.defaultChatAgent?.providerSetting ?? '',
|
||||
};
|
||||
|
||||
// Void commented this out - copilot head
|
||||
// Add next to the command center if command center is disabled
|
||||
MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
|
||||
/* MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
|
||||
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
|
||||
});
|
||||
|
||||
|
|
@ -666,13 +669,15 @@ 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
|
||||
});
|
||||
}); */
|
||||
|
||||
registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction {
|
||||
constructor() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan
|
|||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { InlineCompletion, } from '../../../../editor/common/languages.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
|
|
@ -633,8 +632,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
async _provideInlineCompletionItems(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
context: InlineCompletionContext,
|
||||
token: CancellationToken,
|
||||
): Promise<InlineCompletion[]> {
|
||||
|
||||
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
|
||||
|
|
@ -852,7 +849,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
onAbort: () => { },
|
||||
onAbort: () => { reject('Aborted autocomplete') },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
|
|
@ -897,9 +894,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
) {
|
||||
super()
|
||||
|
||||
this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
this._register(this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
provideInlineCompletions: async (model, position, context, token) => {
|
||||
const items = await this._provideInlineCompletionItems(model, position, context, token)
|
||||
const items = await this._provideInlineCompletionItems(model, position)
|
||||
|
||||
// console.log('item: ', items?.[0]?.insertText)
|
||||
return { items: items, }
|
||||
|
|
@ -936,7 +933,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
});
|
||||
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ 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, voidTools } from '../common/prompt/prompts.js';
|
||||
import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.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 { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, InternalToolInfo } from '../common/toolsServiceTypes.js';
|
||||
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
|
||||
import { IToolsService } from './toolsService.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
|
|
@ -37,6 +37,7 @@ 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';
|
||||
|
||||
|
||||
/*
|
||||
|
|
@ -61,28 +62,6 @@ A checkpoint appears before every LLM message, and before every user message (be
|
|||
*/
|
||||
|
||||
|
||||
const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => {
|
||||
const llmChatMessages: LLMChatMessage[] = []
|
||||
for (const c of chatMessages) {
|
||||
if (c.role === 'user') {
|
||||
llmChatMessages.push({ role: c.role, content: c.content })
|
||||
}
|
||||
else if (c.role === 'assistant')
|
||||
llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning })
|
||||
else if (c.role === 'tool')
|
||||
llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content })
|
||||
else if (c.role === 'decorative_canceled_tool') { // pass
|
||||
}
|
||||
else if (c.role === 'checkpoint') { // pass
|
||||
}
|
||||
else {
|
||||
throw new Error(`Role ${(c as any).role} not recognized.`)
|
||||
}
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
type UserMessageType = ChatMessage & { role: 'user' }
|
||||
type UserMessageState = UserMessageType['state']
|
||||
const defaultMessageState: UserMessageState = {
|
||||
|
|
@ -139,10 +118,9 @@ export type ThreadStreamState = {
|
|||
|
||||
// streaming related - when streaming message
|
||||
streamingToken?: string;
|
||||
messageSoFar?: string;
|
||||
displayContentSoFar?: string;
|
||||
reasoningSoFar?: string;
|
||||
toolNameSoFar?: string;
|
||||
toolParamsSoFar?: string;
|
||||
toolCallSoFar?: RawToolCallObj;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -380,9 +358,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
else if (behavior === 'set') {
|
||||
this.streamState[threadId] = state
|
||||
}
|
||||
else throw new Error(`setStreamState`)
|
||||
}
|
||||
|
||||
|
||||
this._onDidChangeStreamState.fire({ threadId })
|
||||
}
|
||||
|
||||
|
|
@ -442,7 +420,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
return false
|
||||
}
|
||||
private _updateLatestToolTo = (threadId: string, tool: ChatMessage & { role: 'tool' }) => {
|
||||
private _updateLatestTool = (threadId: string, tool: ChatMessage & { role: 'tool' }) => {
|
||||
const swapped = this._swapOutLatestStreamingToolWithResult(threadId, tool)
|
||||
if (swapped) return
|
||||
this._addMessageToThread(threadId, tool)
|
||||
|
|
@ -452,33 +430,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
|
||||
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||
if (!(
|
||||
lastMsg.role === 'tool' && (lastMsg.type === 'tool_request')
|
||||
)) return // should never happen
|
||||
|
||||
const lastUserMsgIdx = findLastIdx(thread.messages, m => m.role === 'user')
|
||||
const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' }
|
||||
if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen
|
||||
|
||||
const instructions = lastUserMessage.displayContent || ''
|
||||
|
||||
const callThisToolFirst: ToolMessage<ToolName> = lastMsg
|
||||
|
||||
this._updateLatestToolTo(threadId, {
|
||||
role: 'tool',
|
||||
type: 'running_now',
|
||||
name: lastMsg.name,
|
||||
paramsStr: lastMsg.paramsStr,
|
||||
id: lastMsg.id,
|
||||
params: lastMsg.params,
|
||||
content: '(value not received yet...)', // this typically shouldn't ever get read
|
||||
result: null
|
||||
})
|
||||
|
||||
this._wrapRunAgentToNotify(
|
||||
this._runChatAgent({ callThisToolFirst, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() })
|
||||
this._runChatAgent({ callThisToolFirst, threadId, ...this._currentModelSelectionProps() })
|
||||
, threadId
|
||||
)
|
||||
}
|
||||
|
|
@ -494,29 +454,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
else return
|
||||
|
||||
const { name, paramsStr, id } = lastMsg
|
||||
const { name } = lastMsg
|
||||
|
||||
const errorMessage = this.errMsgs.rejected
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, paramsStr: paramsStr, id, content: errorMessage, result: null })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null })
|
||||
this._setStreamState(threadId, {}, 'set')
|
||||
}
|
||||
|
||||
// private _rejectLatestStreamingTool(threadId: string) {
|
||||
// const thread = this.state.allThreads[threadId]
|
||||
// if (!thread) return // should never happen
|
||||
|
||||
// const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
// if (lastMessage.role !== 'tool') return
|
||||
// const { name, paramsStr, id, result } = lastMessage
|
||||
// if (result.type !== 'running_now') return
|
||||
// const { params } = result
|
||||
|
||||
// const errorMessage = this.errMsgs.rejected
|
||||
// this._swapOutLatestStreamingToolWithResult(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, })
|
||||
// this._setStreamState(threadId, {}, 'set')
|
||||
|
||||
// }
|
||||
|
||||
stopRunning(threadId: string) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
|
@ -531,19 +475,20 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const isRunning = this.streamState[threadId]?.isRunning
|
||||
if (isRunning === 'LLM') {
|
||||
// abort the stream first so it doesn't change any state
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const displayContentSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
const toolInProgress = this.streamState[threadId]?.toolNameSoFar
|
||||
console.log('toolInProgress', toolInProgress)
|
||||
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
|
||||
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
|
||||
|
||||
if (toolInProgress) {
|
||||
this._addMessageToThread(threadId, { role: 'decorative_canceled_tool', name: toolInProgress })
|
||||
if (toolCallSoFar) {
|
||||
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
}
|
||||
|
||||
this._addUserCheckpoint({ threadId })
|
||||
}
|
||||
|
||||
this._setStreamState(threadId, {}, 'set')
|
||||
|
|
@ -551,18 +496,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
|
||||
private _tools = (chatMode: ChatMode) => {
|
||||
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))
|
||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
||||
: undefined
|
||||
|
||||
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
|
||||
return tools
|
||||
}
|
||||
|
||||
|
||||
|
||||
private readonly errMsgs = {
|
||||
rejected: 'Tool call was rejected by the user.',
|
||||
errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
|
||||
|
|
@ -571,140 +504,163 @@ 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 },
|
||||
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
||||
|
||||
// compute these below
|
||||
let toolParams: ToolCallParams[ToolName]
|
||||
let toolResult: Awaited<ToolResultType[typeof toolName]>
|
||||
let toolResultStr: string
|
||||
|
||||
if (!opts.preapproved) { // skip this if pre-approved
|
||||
// 1. validate tool params
|
||||
try {
|
||||
const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, })
|
||||
return {}
|
||||
}
|
||||
// once validated, add checkpoint for edit
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
const toolRequiresApproval = toolNamesThatRequireApproval.has(toolName)
|
||||
if (toolRequiresApproval) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove
|
||||
// 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 })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
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 })
|
||||
|
||||
let interrupted = false
|
||||
try {
|
||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||
this._currentlyRunningToolInterruptor[threadId] = () => {
|
||||
interrupted = true;
|
||||
interruptTool?.();
|
||||
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) { 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, })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 4. stringify the result to give to the LLM
|
||||
try {
|
||||
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, })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
|
||||
|
||||
return {}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
private async _runChatAgent({
|
||||
threadId,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
userMessageContent,
|
||||
callThisToolFirst,
|
||||
}: {
|
||||
threadId: string,
|
||||
modelSelection: ModelSelection | null,
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined,
|
||||
userMessageContent: string, // content of LATEST user message
|
||||
|
||||
callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' }
|
||||
}) {
|
||||
const userMessageFullContent = userMessageContent
|
||||
const getLatestMessages = async () => {
|
||||
// replace last userMessage with userMessageFullContent (which contains all the files too)
|
||||
const thread = this.state.allThreads[threadId]
|
||||
const latestMessages = thread?.messages ?? []
|
||||
const messages_ = toLLMChatMessages(latestMessages)
|
||||
const lastUserMsgIdx = findLastIdx(messages_, m => m.role === 'user')
|
||||
if (lastUserMsgIdx === -1) return [] // should never happen (or how did they send the message?!)
|
||||
|
||||
// system message
|
||||
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 { wasCutOff, str: directoryStr_ } = await this._directoryStrService.getAllDirectoriesStr()
|
||||
|
||||
const directoryStr = wasCutOff ? (
|
||||
chatMode === 'agent' || chatMode === 'gather' ? `${directoryStr_}\nString cut off, use tools to read more.`
|
||||
: `${directoryStr_}\nString cut off, ask user for more if necessary.`
|
||||
) : directoryStr_
|
||||
|
||||
const runningTerminalIds = this._terminalToolService.listTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode })
|
||||
|
||||
// all messages so far in the chat history (including tools)
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: systemMessage, },
|
||||
...messages_.slice(0, lastUserMsgIdx),
|
||||
{ role: 'user', content: userMessageFullContent },
|
||||
...messages_.slice(lastUserMsgIdx + 1, Infinity),
|
||||
]
|
||||
// console.log('MESSAGES!!!', messages)
|
||||
return messages
|
||||
}
|
||||
|
||||
|
||||
|
||||
// returns true when the tool call is waiting for user approval
|
||||
const handleToolCall = async (
|
||||
tool: ToolCallType,
|
||||
opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] },
|
||||
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
||||
const toolName: ToolName = tool.name
|
||||
const toolParamsStr = tool.paramsStr
|
||||
const toolId = tool.id
|
||||
|
||||
// compute these below
|
||||
let toolParams: ToolCallParams[ToolName]
|
||||
let toolResult: ToolResultType[typeof toolName]
|
||||
let toolResultStr: string
|
||||
|
||||
if (!opts?.preapproved) { // skip this if pre-approved
|
||||
// 1. validate tool params
|
||||
try {
|
||||
const params = await this._toolsService.validateParams[toolName](toolParamsStr)
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
|
||||
return {}
|
||||
}
|
||||
// once validated, add checkpoint for edit
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
const requiresApproval = toolNamesThatRequireApproval.has(toolName)
|
||||
if (requiresApproval) {
|
||||
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, paramsStr: toolParamsStr, params: toolParams, id: toolId })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
toolParams = opts.toolParams
|
||||
}
|
||||
|
||||
// 3. call the tool
|
||||
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
|
||||
let interrupted = false
|
||||
try {
|
||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||
this._currentlyRunningToolInterruptor[threadId] = () => {
|
||||
interrupted = true;
|
||||
interruptTool?.();
|
||||
delete this._currentlyRunningToolInterruptor[threadId];
|
||||
}
|
||||
toolResult = await result // ts is bad... await is needed
|
||||
}
|
||||
catch (error) {
|
||||
if (interrupted) {
|
||||
// the tool result is added when we stop running
|
||||
return { interrupted: true }
|
||||
}
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 4. stringify the result to give to the LLM
|
||||
try {
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
} catch (error) {
|
||||
const errorMessage = this.errMsgs.errWhenStringifying(error)
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, })
|
||||
|
||||
return {}
|
||||
};
|
||||
|
||||
// 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
|
||||
const tools = this._tools(chatMode)
|
||||
|
||||
// clear any previous error
|
||||
this._setStreamState(threadId, { error: undefined }, 'set')
|
||||
|
|
@ -716,7 +672,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
const { interrupted } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params })
|
||||
if (interrupted) return
|
||||
}
|
||||
|
||||
|
|
@ -727,34 +683,39 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
isRunningWhenEnd = undefined
|
||||
nMessagesSent += 1
|
||||
|
||||
let resMessageIsDonePromise: (toolCalls?: ToolCallType[] | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<ToolCallType[] | undefined>((res, rej) => { resMessageIsDonePromise = res })
|
||||
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
|
||||
|
||||
// send llm message
|
||||
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
|
||||
const messages = await getLatestMessages()
|
||||
const systemMessage = await this._generateSystemMessage(chatMode)
|
||||
const llmMessages = await this._generateLLMMessages(threadId)
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: systemMessage },
|
||||
...llmMessages
|
||||
]
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
messages,
|
||||
tools: tools,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||
onText: ({ fullText, fullReasoning, fullToolName, fullToolParams }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolNameSoFar: fullToolName, toolParamsSoFar: fullToolParams }, 'merge')
|
||||
onText: ({ fullText, fullReasoning, toolCall }) => {
|
||||
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
|
||||
},
|
||||
onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
|
||||
// added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning)
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolNameSoFar: undefined, toolParamsSoFar: undefined }, 'merge')
|
||||
// resolve with tool calls
|
||||
resMessageIsDonePromise(toolCalls)
|
||||
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning })
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
||||
},
|
||||
onError: (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||
// add assistant's message to chat history, and clear selection
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
|
||||
this._setStreamState(threadId, { error }, 'set')
|
||||
resMessageIsDonePromise()
|
||||
},
|
||||
|
|
@ -774,14 +735,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
break
|
||||
}
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
const toolCalls = await messageIsDonePromise // wait for message to complete
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
if (aborted) { return }
|
||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
||||
|
||||
// call tool if there is one
|
||||
const tool: ToolCallType | undefined = toolCalls?.[0]
|
||||
const tool: RawToolCallObj | undefined = toolCall
|
||||
if (tool) {
|
||||
const { awaitingUserApproval, interrupted } = await handleToolCall(tool)
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, { 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
|
||||
|
|
@ -994,7 +955,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 })
|
||||
|
|
@ -1089,6 +1050,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',
|
||||
|
|
@ -1118,14 +1080,21 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
if (!thread) return // should never happen
|
||||
|
||||
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken // currently streaming LLM on this thread
|
||||
if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning === 'LLM') {
|
||||
// if about to call the other LLM, just wait for it by stopping right now
|
||||
return
|
||||
}
|
||||
// stop it (this simply resolves the promise to free up space)
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
|
||||
|
||||
|
||||
// add dummy before this message to keep checkpoint before user message idea consistent
|
||||
if (thread.messages.length === 0) {
|
||||
this._addUserCheckpoint({ threadId })
|
||||
}
|
||||
|
||||
// if the current thread is already streaming, stop it (this simply resolves the promise to free up space)
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
|
||||
const { chatMode } = this._settingsService.state.globalSettings
|
||||
|
||||
|
|
@ -1141,7 +1110,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
this._setThreadState(threadId, { currCheckpointIdx: null }) // no longer at a checkpoint because started streaming
|
||||
|
||||
this._wrapRunAgentToNotify(
|
||||
this._runChatAgent({ threadId, userMessageContent, ...this._currentModelSelectionProps(), }),
|
||||
this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }),
|
||||
threadId,
|
||||
)
|
||||
}
|
||||
|
|
@ -1245,7 +1214,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
// else search codebase for `target`
|
||||
let uris: URI[] = []
|
||||
try {
|
||||
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, include: null, pageNumber: 0 })
|
||||
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, searchInFolder: null, pageNumber: 0 })
|
||||
uris = result.uris
|
||||
} catch (e) {
|
||||
return null
|
||||
|
|
@ -1517,6 +1486,10 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
}
|
||||
}
|
||||
}, 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
|
||||
|
|
|
|||
|
|
@ -14,19 +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_CHARS_TOTAL_BEGINNING = 20_000
|
||||
const MAX_CHARS_TOTAL_TOOL = 20_000
|
||||
// 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<{ wasCutOff: boolean, str: string }>
|
||||
getAllDirectoriesStr(): Promise<{ wasCutOff: boolean, str: string }>
|
||||
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
|
||||
|
||||
}
|
||||
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
|
||||
|
|
@ -54,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;
|
||||
}
|
||||
|
||||
|
|
@ -129,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 };
|
||||
};
|
||||
|
||||
|
|
@ -270,23 +325,54 @@ 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_CHARS_TOTAL_TOOL);
|
||||
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
|
||||
return {
|
||||
str: `Directory of ${uri.fsPath}:\n${content}`,
|
||||
wasCutOff,
|
||||
// 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}`
|
||||
if (wasCutOff) c = `${c}\n...Result was truncated...`
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
async getAllDirectoriesStr() {
|
||||
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';
|
||||
|
|
@ -299,18 +385,45 @@ 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);
|
||||
console.log('dirtree', dirTree)
|
||||
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log('cutoff!!!!!!!', str, cutOffMessage)
|
||||
|
||||
return { wasCutOff: cutOff, str };
|
||||
if (cutOff) {
|
||||
return `${str}\n${cutOffMessage}`
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApply
|
|||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { FeatureName } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { ITextFileService } from '../../../services/textfile/common/textfiles.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';
|
||||
|
|
@ -72,6 +71,21 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
|
|||
|
||||
const numLinesOfStr = (str: string) => str.split('\n').length
|
||||
|
||||
|
||||
export const getLengthOfTextPx = ({ tabWidth, spaceWidth, content }: { tabWidth: number, spaceWidth: number, content: string }) => {
|
||||
let lengthOfTextPx = 0;
|
||||
for (const char of content) {
|
||||
if (char === '\t') {
|
||||
lengthOfTextPx += tabWidth
|
||||
} else {
|
||||
lengthOfTextPx += spaceWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return lengthOfTextPx
|
||||
}
|
||||
|
||||
|
||||
const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
|
||||
|
||||
const model = editor.getModel();
|
||||
|
|
@ -95,16 +109,14 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
|
|||
const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth;
|
||||
const tabWidth = numSpacesInTab * spaceWidth;
|
||||
|
||||
let paddingLeft = 0;
|
||||
for (const char of leadingWhitespace) {
|
||||
if (char === '\t') {
|
||||
paddingLeft += tabWidth
|
||||
} else if (char === ' ') {
|
||||
paddingLeft += spaceWidth;
|
||||
}
|
||||
}
|
||||
const leftWhitespacePx = getLengthOfTextPx({
|
||||
tabWidth,
|
||||
spaceWidth,
|
||||
content: leadingWhitespace
|
||||
});
|
||||
|
||||
return paddingLeft;
|
||||
|
||||
return leftWhitespacePx;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -190,7 +202,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
// @IFileService private readonly _fileService: IFileService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
|
@ -720,16 +731,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
resource: uri,
|
||||
label: 'Void Agent',
|
||||
code: 'undoredo.editCode',
|
||||
undo: () => { opts?.onWillUndo?.(); this._restoreVoidFileSnapshot(uri, beforeSnapshot); },
|
||||
redo: () => { if (afterSnapshot) this._restoreVoidFileSnapshot(uri, afterSnapshot) }
|
||||
undo: async () => { opts?.onWillUndo?.(); await this._restoreVoidFileSnapshot(uri, beforeSnapshot) },
|
||||
redo: async () => { if (afterSnapshot) await this._restoreVoidFileSnapshot(uri, afterSnapshot) }
|
||||
}
|
||||
this._undoRedoService.pushElement(elt)
|
||||
|
||||
const onFinishEdit = async () => {
|
||||
afterSnapshot = this._getCurrentVoidFileSnapshot(uri)
|
||||
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
|
||||
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
|
||||
})
|
||||
await this._voidModelService.saveModel(uri)
|
||||
}
|
||||
return { onFinishEdit }
|
||||
}
|
||||
|
|
@ -1105,6 +1114,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
await this._voidModelService.saveModel(uri) // save the URI
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1400,6 +1410,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText: fullText_ } = params
|
||||
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
|
||||
|
|
@ -1586,7 +1597,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const N_RETRIES = 5
|
||||
const N_RETRIES = 2
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runSearchReplace = async () => {
|
||||
|
|
@ -1617,6 +1628,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
// blocks are [done done done ... {writingFinal|writingOriginal}]
|
||||
|
|
@ -1876,6 +1888,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
interruptURIStreaming({ uri }: { uri: URI }) {
|
||||
if (!this._uriIsStreaming(uri)) return
|
||||
this._undoHistory(uri)
|
||||
// brute force for now is OK
|
||||
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
|
@ -1883,7 +1897,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
if (!diffArea._streamState.isStreaming) continue
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
this._undoHistory(uri)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -76,93 +76,107 @@
|
|||
opacity: 80%;
|
||||
}
|
||||
|
||||
/* styles for all containers used by void */
|
||||
.void-scope {
|
||||
--scrollbar-vertical-width: 8px;
|
||||
--scrollbar-horizontal-height: 6px;
|
||||
}
|
||||
|
||||
/* Target both void-scope and all its descendants with scrollbars */
|
||||
.void-scope,
|
||||
.void-scope * {
|
||||
scrollbar-width: thin !important;
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar,
|
||||
.void-scope *::-webkit-scrollbar {
|
||||
width: var(--scrollbar-vertical-width) !important;
|
||||
height: var(--scrollbar-horizontal-height) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb,
|
||||
.void-scope *::-webkit-scrollbar-thumb {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
border-radius: 4px !important;
|
||||
border: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb:hover,
|
||||
.void-scope *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb:active,
|
||||
.void-scope *::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.2) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-track,
|
||||
.void-scope *::-webkit-scrollbar-track {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-corner,
|
||||
.void-scope *::-webkit-scrollbar-corner {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
/* Add void-scrollable-element styles to match */
|
||||
.void-scrollable-element {
|
||||
background-color: var(--vscode-editor-background);
|
||||
--scrollbar-vertical-width: 14px;
|
||||
--scrollbar-horizontal-height: 6px;
|
||||
overflow: auto; /* Ensure scrollbars are shown when needed */
|
||||
}
|
||||
|
||||
.void-scrollable-element,
|
||||
.void-scrollable-element * {
|
||||
scrollbar-width: thin !important; /* For Firefox */
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar,
|
||||
.void-scrollable-element *::-webkit-scrollbar {
|
||||
width: 14px !important;
|
||||
height: 4px !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-track,
|
||||
.void-scrollable-element *::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
width: var(--scrollbar-vertical-width) !important;
|
||||
height: var(--scrollbar-horizontal-height) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb {
|
||||
background-color: transparent !important;
|
||||
border-radius: 0px !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
border-radius: 4px !important;
|
||||
border: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:active,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.2) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-track,
|
||||
.void-scrollable-element *::-webkit-scrollbar-track {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-corner,
|
||||
.void-scrollable-element *::-webkit-scrollbar-corner {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
|
||||
background-color: var(--vscode-scrollbarSlider-background) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function saveStylesFile() {
|
|||
} catch (err) {
|
||||
console.error('[scope-tailwind] Error saving styles.css:', err);
|
||||
}
|
||||
}, 4000);
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { URI } from '../../../../../../../base/common/uri.js'
|
|||
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
|
||||
import { Check, X, Square, Copy, Play, } from 'lucide-react'
|
||||
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { PlacesType, VariantType } from 'react-tooltip'
|
||||
|
||||
enum CopyButtonText {
|
||||
Idle = 'Copy',
|
||||
|
|
@ -20,30 +21,28 @@ enum CopyButtonText {
|
|||
|
||||
|
||||
type IconButtonProps = {
|
||||
onClick: () => void;
|
||||
Icon: LucideIcon
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => (
|
||||
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick?.();
|
||||
onClick?.(e);
|
||||
}}
|
||||
// border border-void-border-1 rounded
|
||||
className={`
|
||||
size-[22px]
|
||||
p-[4px]
|
||||
size-[18px]
|
||||
p-[2px]
|
||||
flex items-center justify-center
|
||||
text-sm bg-void-bg-3 text-void-fg-1
|
||||
text-sm bg-void-bg-3 text-void-fg-3
|
||||
hover:brightness-110
|
||||
border border-void-border-1 rounded
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<Icon />
|
||||
</button>
|
||||
|
|
@ -94,13 +93,14 @@ export const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
|||
return <IconShell1
|
||||
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
|
||||
onClick={onCopy}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Copy' })}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
|
||||
export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
|
|
@ -110,6 +110,8 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
|
|||
onClick={() => {
|
||||
commandService.executeCommand('vscode.open', uri, { preview: true })
|
||||
}}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return jumpToFileButton
|
||||
|
|
@ -122,7 +124,6 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
|
|||
<IconShell1
|
||||
Icon={Terminal}
|
||||
onClick={onClick}
|
||||
className="text-void-fg-1"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -163,10 +164,11 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
|
|||
rerender(c => c + 1)
|
||||
console.log('rerendering....')
|
||||
}
|
||||
}, [applyBoxId, applyBoxId, uri]))
|
||||
}, [applyBoxId, uri]))
|
||||
|
||||
const currStreamState = getStreamState()
|
||||
|
||||
|
||||
return {
|
||||
getStreamState,
|
||||
isDisabled,
|
||||
|
|
@ -175,22 +177,61 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
|
|||
}
|
||||
|
||||
|
||||
export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
|
||||
type IndicatorColor = 'green' | 'orange' | 'dark' | 'yellow' | null
|
||||
export const StatusIndicator = ({ indicatorColor, title, className, ...props }: { indicatorColor: IndicatorColor, title?: React.ReactNode, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div className={`flex flex-row text-void-fg-3 text-xs items-center gap-1.5 ${className}`} {...props}>
|
||||
{title && <span className='opacity-80'>{title}</span>}
|
||||
<div
|
||||
className={` size-1.5 rounded-full border
|
||||
${indicatorColor === 'dark' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
indicatorColor === 'orange' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
|
||||
indicatorColor === 'green' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
indicatorColor === 'yellow' ? 'bg-yellow-500 border-yellow-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
'bg-void-border-1 border-void-border-1'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position = 'top', offset = undefined }: { tooltipName: string, color?: IndicatorColor, position?: PlacesType, offset?: number }) => ({
|
||||
'data-tooltip-id': color === 'orange' ? `void-tooltip-orange` : color === 'green' ? 'void-tooltip-green' : 'void-tooltip',
|
||||
'data-tooltip-place': position as PlacesType,
|
||||
'data-tooltip-content': `${tooltipName}`,
|
||||
'data-tooltip-offset': offset,
|
||||
})
|
||||
|
||||
|
||||
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
return <div className='flex flex-row items-center min-h-4 max-h-4 min-w-4 max-w-4'>
|
||||
<div
|
||||
className={` size-1.5 rounded-full border
|
||||
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
|
||||
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
'bg-void-border-1 border-void-border-1'
|
||||
}`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
const color = (
|
||||
currStreamState === 'idle-no-changes' ? 'dark' :
|
||||
currStreamState === 'streaming' ? 'orange' :
|
||||
currStreamState === 'idle-has-changes' ? 'green' :
|
||||
null
|
||||
)
|
||||
|
||||
const tooltipName = (
|
||||
currStreamState === 'idle-no-changes' ? 'Done' :
|
||||
currStreamState === 'streaming' ? 'Applying' :
|
||||
currStreamState === 'idle-has-changes' ? 'Done' : // also 'Done'? 'Applied' looked bad
|
||||
''
|
||||
)
|
||||
|
||||
const statusIndicatorHTML = <StatusIndicator
|
||||
key={currStreamState}
|
||||
className='mx-2'
|
||||
indicatorColor={color}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName, color, position: 'top', offset: 12 })}
|
||||
/>
|
||||
return statusIndicatorHTML
|
||||
}
|
||||
|
||||
|
||||
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
|
|
@ -216,7 +257,10 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
|
|||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
|
||||
applyDonePromise?.catch(e => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
|
||||
})
|
||||
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
|
||||
|
|
@ -251,11 +295,22 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
|
|||
|
||||
|
||||
if (currStreamState === 'streaming') {
|
||||
return <IconShell1 Icon={Square} onClick={onInterrupt} />
|
||||
return <IconShell1
|
||||
|
||||
Icon={Square}
|
||||
onClick={onInterrupt}
|
||||
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-no-changes') {
|
||||
return <IconShell1 Icon={reapplyIcon ? RotateCw : Play} onClick={onClickSubmit} />
|
||||
|
||||
return <IconShell1
|
||||
Icon={reapplyIcon ? RotateCw : Play}
|
||||
onClick={onClickSubmit}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: reapplyIcon ? 'Reapply' : 'Apply' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
|
|
@ -267,19 +322,18 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
|
|||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
className="text-red-600"
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Reject file' })}
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
className="text-green-600"
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Accept file' })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const BlockCodeApplyWrapper = ({
|
||||
children,
|
||||
initValue,
|
||||
|
|
@ -314,7 +368,7 @@ export const BlockCodeApplyWrapper = ({
|
|||
{/* header */}
|
||||
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
|
||||
<div className="flex items-center">
|
||||
<StatusIndicatorHTML uri={uri} applyBoxId={applyBoxId} />
|
||||
<StatusIndicatorForApplyButton uri={uri} applyBoxId={applyBoxId} />
|
||||
<span className="text-[13px] font-light text-void-fg-3">
|
||||
{name}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { QuickEditPropsType } from '../../../quickEditActions.js';
|
|||
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { useRefState } from '../util/helpers.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
|
||||
export const QuickEditChat = ({
|
||||
|
|
@ -89,8 +88,6 @@ export const QuickEditChat = ({
|
|||
editCodeService.removeCtrlKZone({ diffareaid })
|
||||
}, [editCodeService, diffareaid])
|
||||
|
||||
useScrollbarStyles(sizerRef)
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
|
||||
|
||||
const chatAreaRef = useRef<HTMLDivElement | null>(null)
|
||||
|
|
|
|||
|
|
@ -15,17 +15,21 @@ import { ErrorDisplay } from './ErrorDisplay.js';
|
|||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
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, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react';
|
||||
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X } from 'lucide-react';
|
||||
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
|
||||
import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.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 { error } from 'console';
|
||||
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -350,7 +354,7 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
|||
<div className='flex flex-col gap-y-1'>
|
||||
<ReasoningOptionSlider featureName={featureName} />
|
||||
|
||||
<div className='flex items-center flex-wrap gap-x-2 gap-y-1'>
|
||||
<div className='flex items-center flex-wrap gap-x-2 gap-y-1 text-nowrap flex-nowrap'>
|
||||
{featureName === 'Chat' && <ChatModeDropdown className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-2 rounded py-0.5 px-1' />}
|
||||
<ModelDropdown featureName={featureName} className='text-xs text-void-fg-3 bg-void-bg-1 rounded' />
|
||||
</div>
|
||||
|
|
@ -391,6 +395,9 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re
|
|||
${disabled ? 'bg-vscode-disabled-fg cursor-default' : 'bg-white cursor-pointer'}
|
||||
${className}
|
||||
`}
|
||||
// data-tooltip-id='void-tooltip'
|
||||
// data-tooltip-content={'Send'}
|
||||
// data-tooltip-place='left'
|
||||
{...props}
|
||||
>
|
||||
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[2px]" />
|
||||
|
|
@ -653,6 +660,7 @@ type ToolHeaderParams = {
|
|||
numResults?: number;
|
||||
hasNextPage?: boolean;
|
||||
children?: React.ReactNode;
|
||||
bottomChildren?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
isOpen?: boolean,
|
||||
}
|
||||
|
|
@ -665,11 +673,13 @@ const ToolHeaderWrapper = ({
|
|||
numResults,
|
||||
hasNextPage,
|
||||
children,
|
||||
bottomChildren,
|
||||
isError,
|
||||
onClick,
|
||||
isOpen,
|
||||
isRejected,
|
||||
}: ToolHeaderParams) => {
|
||||
className, // applies to the main content
|
||||
}: ToolHeaderParams & { className?: string }) => {
|
||||
|
||||
const [isOpen_, setIsOpen] = useState(false);
|
||||
const isExpanded = isOpen !== undefined ? isOpen : isOpen_
|
||||
|
|
@ -678,25 +688,28 @@ 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] ${isClickable ? 'cursor-pointer' : ''} ${!isDropdown ? 'mx-1' : ''}`}
|
||||
onClick={() => {
|
||||
if (isDropdown) { setIsOpen(v => !v); }
|
||||
if (onClick) { onClick(); }
|
||||
}}
|
||||
>
|
||||
{isDropdown && (
|
||||
<ChevronRight
|
||||
className={`text-void-fg-3 mr-0.5 h-4 w-4 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
)}
|
||||
<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' : ''}`}>
|
||||
{/* left */}
|
||||
<div className={`flex items-center gap-x-2 min-w-0 overflow-hidden ${isClickable ? 'hover:brightness-125 transition-all duration-150' : ''}`}>
|
||||
<div className={`
|
||||
flex items-center min-w-0 overflow-hidden grow
|
||||
${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (isDropdown) { setIsOpen(v => !v); }
|
||||
if (onClick) { onClick(); }
|
||||
}}
|
||||
>
|
||||
{isDropdown && (<ChevronRight
|
||||
className={`
|
||||
text-void-fg-3 mr-0.5 h-4 w-4 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)]
|
||||
${isExpanded ? 'rotate-90' : ''}
|
||||
`}
|
||||
/>)}
|
||||
<span className="text-void-fg-3 flex-shrink-0">{title}</span>
|
||||
<span className="text-void-fg-4 text-xs italic truncate">{desc1}</span>
|
||||
<span className="text-void-fg-4 text-xs italic truncate ml-2">{desc1}</span>
|
||||
</div>
|
||||
|
||||
{/* right */}
|
||||
|
|
@ -724,6 +737,7 @@ const ToolHeaderWrapper = ({
|
|||
{children}
|
||||
</div>}
|
||||
</div>
|
||||
{bottomChildren}
|
||||
</div>);
|
||||
};
|
||||
|
||||
|
|
@ -772,7 +786,7 @@ const SimplifiedToolHeader = ({
|
|||
|
||||
|
||||
|
||||
const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => {
|
||||
const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, currCheckpointIdx, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, currCheckpointIdx: number | undefined, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
|
@ -923,7 +937,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
|
|||
</VoidChatArea>
|
||||
}
|
||||
|
||||
|
||||
const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1
|
||||
|
||||
return <div
|
||||
// align chatbubble accoridng to role
|
||||
|
|
@ -933,7 +947,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
|
|||
: mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre
|
||||
}
|
||||
|
||||
${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}
|
||||
${isCheckpointGhost && !isMsgAfterCheckpoint ? 'opacity-50 pointer-events-none' : ''}
|
||||
`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
|
|
@ -952,25 +966,32 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
|
|||
</div>
|
||||
|
||||
|
||||
<EditSymbol
|
||||
size={18}
|
||||
className={`
|
||||
absolute -top-1 -right-1
|
||||
translate-x-0 -translate-y-0
|
||||
cursor-pointer z-1
|
||||
p-[2px]
|
||||
bg-void-bg-1 border border-void-border-1 rounded-md
|
||||
transition-opacity duration-200 ease-in-out
|
||||
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (mode === 'display') {
|
||||
onOpenEdit()
|
||||
} else if (mode === 'edit') {
|
||||
onCloseEdit()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute -top-1 -right-1 translate-x-0 -translate-y-0 z-1"
|
||||
// data-tooltip-id='void-tooltip'
|
||||
// data-tooltip-content='Edit message'
|
||||
// data-tooltip-place='left'
|
||||
>
|
||||
<EditSymbol
|
||||
size={18}
|
||||
className={`
|
||||
cursor-pointer
|
||||
p-[2px]
|
||||
bg-void-bg-1 border border-void-border-1 rounded-md
|
||||
transition-opacity duration-200 ease-in-out
|
||||
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (mode === 'display') {
|
||||
onOpenEdit()
|
||||
} else if (mode === 'edit') {
|
||||
onCloseEdit()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -1023,6 +1044,7 @@ const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => {
|
|||
prose-blockquote:pl-2
|
||||
prose-blockquote:my-2
|
||||
|
||||
prose-code:text-void-fg-3
|
||||
prose-code:text-[12px]
|
||||
prose-code:before:content-none
|
||||
prose-code:after:content-none
|
||||
|
|
@ -1074,7 +1096,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
|
|||
|
||||
const reasoningStr = chatMessage.reasoning?.trim() || null
|
||||
const hasReasoning = !!reasoningStr
|
||||
const isDoneReasoning = !!chatMessage.content
|
||||
const isDoneReasoning = !!chatMessage.displayContent
|
||||
const thread = chatThreadsService.getCurrentThread()
|
||||
|
||||
|
||||
|
|
@ -1083,7 +1105,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
|
|||
messageIdx: messageIdx,
|
||||
}
|
||||
|
||||
const isEmpty = !chatMessage.content && !chatMessage.reasoning
|
||||
const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning
|
||||
if (isEmpty) return null
|
||||
|
||||
return <>
|
||||
|
|
@ -1107,7 +1129,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
|
|||
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<ProseWrapper>
|
||||
<ChatMarkdownRender
|
||||
string={chatMessage.content || ''}
|
||||
string={chatMessage.displayContent || ''}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
isApplyEnabled={true}
|
||||
isLinkDetectionEnabled={true}
|
||||
|
|
@ -1236,7 +1258,7 @@ const ToolRequestAcceptRejectButtons = () => {
|
|||
<button
|
||||
onClick={onAccept}
|
||||
className={`
|
||||
px-4 py-1.5
|
||||
px-2 py-1
|
||||
bg-[var(--vscode-button-background)]
|
||||
text-[var(--vscode-button-foreground)]
|
||||
hover:bg-[var(--vscode-button-hoverBackground)]
|
||||
|
|
@ -1252,7 +1274,7 @@ const ToolRequestAcceptRejectButtons = () => {
|
|||
<button
|
||||
onClick={onReject}
|
||||
className={`
|
||||
px-4 py-1.5
|
||||
px-2 py-1
|
||||
bg-[var(--vscode-button-secondaryBackground)]
|
||||
text-[var(--vscode-button-secondaryForeground)]
|
||||
hover:bg-[var(--vscode-button-secondaryHoverBackground)]
|
||||
|
|
@ -1267,7 +1289,7 @@ const ToolRequestAcceptRejectButtons = () => {
|
|||
const autoApproveToggle = (
|
||||
<div className="flex items-center ml-2 gap-x-1">
|
||||
<VoidSwitch
|
||||
size="xs"
|
||||
size="xxs"
|
||||
value={voidSettingsState.globalSettings.autoApprove}
|
||||
onChange={onToggleAutoApprove}
|
||||
/>
|
||||
|
|
@ -1290,7 +1312,7 @@ export const ToolChildrenWrapper = ({ children, className }: { children: React.R
|
|||
</div>
|
||||
}
|
||||
export const CodeChildren = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className='bg-void-bg-3 p-1 rounded-sm font-mono overflow-auto text-sm'>
|
||||
return <div className='bg-void-bg-3 p-1 rounded-sm overflow-auto text-sm'>
|
||||
<div className='!select-text cursor-auto'>
|
||||
{children}
|
||||
</div>
|
||||
|
|
@ -1313,7 +1335,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} />
|
||||
|
|
@ -1321,10 +1343,35 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript
|
|||
</div>
|
||||
}
|
||||
|
||||
const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
|
||||
|
||||
if (lintErrors.length === 0) return null;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="w-full px-2">
|
||||
<ToolHeaderWrapper className='!border-t-0' title={'Lint errors'} desc1={''} isOpen={isOpen} onClick={() => { setIsOpen(o => !o) }} >
|
||||
<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>
|
||||
</ToolHeaderWrapper>
|
||||
|
||||
</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'>
|
||||
<StatusIndicatorHTML applyBoxId={applyBoxId} uri={uri} />
|
||||
|
||||
|
||||
<StatusIndicatorForApplyButton applyBoxId={applyBoxId} uri={uri} />
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} />}
|
||||
<ApplyButtonsHTML applyBoxId={applyBoxId} uri={uri} codeStr={codeStr} reapplyIcon={true} />
|
||||
|
|
@ -1333,17 +1380,23 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin
|
|||
|
||||
|
||||
|
||||
const InvalidTool = ({ toolName }: { toolName: string }) => {
|
||||
const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const title = getTitle({ name: toolName, type: 'invalid_params' })
|
||||
const desc1 = 'Invalid parameters'
|
||||
const icon = null
|
||||
const isError = true
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
|
||||
componentParams.children = <ToolChildrenWrapper>
|
||||
<CodeChildren>
|
||||
{message}
|
||||
</CodeChildren>
|
||||
</ToolChildrenWrapper>
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
}
|
||||
|
||||
const CanceledTool = ({ toolName }: { toolName: string }) => {
|
||||
const CanceledTool = ({ toolName }: { toolName: ToolName }) => {
|
||||
const accessor = useAccessor()
|
||||
const title = getTitle({ name: toolName, type: 'rejected' })
|
||||
const desc1 = ''
|
||||
|
|
@ -1699,7 +1752,10 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
|
||||
// add children
|
||||
if (toolMessage.type !== 'tool_error') {
|
||||
const { params } = toolMessage
|
||||
const { params, result } = toolMessage
|
||||
|
||||
componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
|
||||
|
||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
|
|
@ -1763,18 +1819,18 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
resolveReason.type === 'toofull' ? `\n(truncated)`
|
||||
: null
|
||||
|
||||
componentParams.children = <ToolChildrenWrapper className='font-mono whitespace-pre text-nowrap overflow-auto text-sm'>
|
||||
componentParams.children = <ToolChildrenWrapper className='whitespace-pre text-nowrap overflow-auto text-sm'>
|
||||
|
||||
<div className='!select-text cursor-auto'>
|
||||
<div>
|
||||
<span>{`Ran command: `}</span>
|
||||
<span className="text-void-fg-1">{command}</span>
|
||||
<span className="text-void-fg-1 font-sans">{`Ran command: `}</span>
|
||||
<span className="font-mono">{command}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{resolveReason.type === 'bgtask' ? 'Result so far:\n' : null}</span>
|
||||
<span>{`Result: `}</span>
|
||||
<span className="text-void-fg-1">{terminalResult}</span>
|
||||
<span className="text-void-fg-1">{additionalDetailsStr}</span>
|
||||
<span className="text-void-fg-1 font-mono">{terminalResult}</span>
|
||||
<span className="text-void-fg-1 font-mono">{additionalDetailsStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ToolChildrenWrapper>
|
||||
|
|
@ -1843,19 +1899,20 @@ type ChatBubbleProps = {
|
|||
isCommitted: boolean,
|
||||
chatIsRunning: IsRunningType,
|
||||
threadId: string,
|
||||
currCheckpointIdx: number,
|
||||
currCheckpointIdx: number | undefined,
|
||||
_scrollToBottom: (() => void) | null,
|
||||
}
|
||||
|
||||
const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
|
||||
const role = chatMessage.role
|
||||
|
||||
const isCheckpointGhost = messageIdx > currCheckpointIdx && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
|
||||
const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
|
||||
|
||||
if (role === 'user') {
|
||||
return <UserMessageComponent
|
||||
chatMessage={chatMessage}
|
||||
isCheckpointGhost={isCheckpointGhost}
|
||||
currCheckpointIdx={currCheckpointIdx}
|
||||
messageIdx={messageIdx}
|
||||
_scrollToBottom={_scrollToBottom}
|
||||
/>
|
||||
|
|
@ -1899,7 +1956,7 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
|
|||
|
||||
if (chatMessage.type === 'invalid_params') {
|
||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<InvalidTool toolName={chatMessage.name} />
|
||||
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
@ -1921,7 +1978,7 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
|
|||
return null
|
||||
}
|
||||
|
||||
else if (role === 'decorative_canceled_tool') {
|
||||
else if (role === 'interrupted_streaming_tool') {
|
||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<CanceledTool toolName={chatMessage.name} />
|
||||
</div>
|
||||
|
|
@ -1942,32 +1999,326 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
|
|||
|
||||
|
||||
|
||||
export const AcceptAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => (
|
||||
<button
|
||||
className={`
|
||||
px-1 py-0.5
|
||||
flex items-center gap-1
|
||||
text-white text-[11px] text-nowrap
|
||||
rounded-md
|
||||
cursor-pointer
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: acceptAllBg,
|
||||
border: acceptBorder,
|
||||
}}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
>
|
||||
{text ? <span>{text}</span> : <Check size={16} />}
|
||||
</button>
|
||||
)
|
||||
|
||||
export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => (
|
||||
<button
|
||||
className={`
|
||||
px-1 py-0.5
|
||||
flex items-center gap-1
|
||||
text-white text-[11px] text-nowrap
|
||||
rounded-md
|
||||
cursor-pointer
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: rejectAllBg,
|
||||
border: rejectBorder,
|
||||
}}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
>
|
||||
{text ? <span>{text}</span> : <X size={16} />}
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
|
||||
const CommandBarInChat = () => {
|
||||
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const { stateOfURI: commandBarStateOfURI, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
const numFilesChanged = sortedCommandBarURIs.length
|
||||
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const chatThreadsState = useChatThreadsState()
|
||||
const chatThreadsStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
|
||||
if (!sortedCommandBarURIs || sortedCommandBarURIs.length === 0) {
|
||||
return null
|
||||
}
|
||||
const [fileDetailsOpenedState, setFileDetailsOpenedState] = useState<'auto-opened' | 'auto-closed' | 'user-opened' | 'user-closed'>('auto-closed');
|
||||
const isFileDetailsOpened = fileDetailsOpenedState === 'auto-opened' || fileDetailsOpenedState === 'user-opened';
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// close the file details if there are no files
|
||||
// this converts 'user-closed' to 'auto-closed'
|
||||
if (numFilesChanged === 0) {
|
||||
setFileDetailsOpenedState('auto-closed')
|
||||
}
|
||||
// open the file details if it hasnt been closed
|
||||
if (numFilesChanged > 0 && fileDetailsOpenedState !== 'user-closed') {
|
||||
setFileDetailsOpenedState('auto-opened')
|
||||
}
|
||||
}, [fileDetailsOpenedState, setFileDetailsOpenedState, numFilesChanged])
|
||||
|
||||
|
||||
const isFinishedMakingThreadChanges = numFilesChanged !== 0 && (chatThreadsStreamState ? !chatThreadsStreamState.isRunning : true)
|
||||
|
||||
// ======== status of agent ========
|
||||
// This icon answers the question "is the LLM doing work on this thread?"
|
||||
// assume it is single threaded for now
|
||||
// green = Running
|
||||
// orange = Requires action
|
||||
// dark = Done
|
||||
|
||||
const threadStatus = (
|
||||
chatThreadsStreamState?.isRunning === 'awaiting_user' ? { title: 'Needs Approval', color: 'yellow', } as const
|
||||
: chatThreadsStreamState?.isRunning ? { title: 'Running', color: 'orange', } as const
|
||||
: { title: 'Done', color: 'dark', } as const
|
||||
)
|
||||
|
||||
|
||||
const threadStatusHTML = <StatusIndicator className='mx-1' indicatorColor={threadStatus.color} title={threadStatus.title} />
|
||||
|
||||
|
||||
// ======== info about changes ========
|
||||
// num files changed
|
||||
// acceptall + rejectall
|
||||
// popup info about each change (each with num changes + acceptall + rejectall of their own)
|
||||
|
||||
const numFilesChangedStr = numFilesChanged === 0 ? 'No files with changes'
|
||||
: `${sortedCommandBarURIs.length} file${numFilesChanged === 1 ? '' : 's'} with changes`
|
||||
|
||||
|
||||
|
||||
|
||||
const acceptRejectAllButtons = <div
|
||||
// do this with opacity so that the height remains the same at all times
|
||||
className={`flex items-center gap-0.5
|
||||
${isFinishedMakingThreadChanges ? '' : 'opacity-0 pointer-events-none'}`
|
||||
}
|
||||
>
|
||||
<IconShell1 // RejectAllButtonWrapper
|
||||
// text="Reject All"
|
||||
// className="text-xs"
|
||||
Icon={X}
|
||||
onClick={() => {
|
||||
sortedCommandBarURIs.forEach(uri => {
|
||||
editCodeService.acceptOrRejectAllDiffAreas({
|
||||
uri,
|
||||
removeCtrlKs: true,
|
||||
behavior: "reject",
|
||||
_addToHistory: true,
|
||||
});
|
||||
});
|
||||
}}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Reject all'
|
||||
/>
|
||||
|
||||
<IconShell1 // AcceptAllButtonWrapper
|
||||
// text="Accept All"
|
||||
// className="text-xs"
|
||||
Icon={Check}
|
||||
onClick={() => {
|
||||
sortedCommandBarURIs.forEach(uri => {
|
||||
editCodeService.acceptOrRejectAllDiffAreas({
|
||||
uri,
|
||||
removeCtrlKs: true,
|
||||
behavior: "accept",
|
||||
_addToHistory: true,
|
||||
});
|
||||
});
|
||||
}}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Accept all'
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
// !select-text cursor-auto
|
||||
const fileDetailsContent = <div className="px-2 gap-1 w-full">
|
||||
{sortedCommandBarURIs.map((uri, i) => {
|
||||
const basename = getBasename(uri.fsPath)
|
||||
|
||||
const { sortedDiffIds, isStreaming } = commandBarStateOfURI[uri.fsPath] ?? {}
|
||||
const isFinishedMakingFileChanges = !isStreaming
|
||||
|
||||
const numDiffs = sortedDiffIds?.length || 0
|
||||
|
||||
const fileStatus = (isFinishedMakingFileChanges
|
||||
? { title: 'Done', color: 'dark', } as const
|
||||
: { title: 'Running', color: 'orange', } as const
|
||||
)
|
||||
|
||||
const fileNameHTML = <div
|
||||
className="flex items-center gap-1.5 text-void-fg-3 hover:brightness-125 transition-all duration-200 cursor-pointer"
|
||||
onClick={() => commandService.executeCommand('vscode.open', uri, { preview: true })}
|
||||
>
|
||||
{/* <FileIcon size={14} className="text-void-fg-3" /> */}
|
||||
<span className="text-void-fg-3">{basename}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
const detailsContent = <div className='flex px-4'>
|
||||
<span className="text-void-fg-3 opacity-80">{numDiffs} diff{numDiffs !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
|
||||
const acceptRejectButtons = <div
|
||||
// do this with opacity so that the height remains the same at all times
|
||||
className={`flex items-center gap-0.5
|
||||
${isFinishedMakingFileChanges ? '' : 'opacity-0 pointer-events-none'}
|
||||
`}
|
||||
>
|
||||
<JumpToFileButton
|
||||
uri={uri}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Go to file'
|
||||
/>
|
||||
<IconShell1 // RejectAllButtonWrapper
|
||||
Icon={X}
|
||||
onClick={() => { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "reject", _addToHistory: true, }); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Reject file'
|
||||
|
||||
/>
|
||||
<IconShell1 // AcceptAllButtonWrapper
|
||||
Icon={Check}
|
||||
onClick={() => { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "accept", _addToHistory: true, }); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Accept file'
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
const fileStatusHTML = <StatusIndicator className='mx-1' indicatorColor={fileStatus.color} title={fileStatus.title} />
|
||||
|
||||
return (
|
||||
// name, details
|
||||
<div key={i} className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
{fileNameHTML}
|
||||
{detailsContent}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{acceptRejectButtons}
|
||||
{fileStatusHTML}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
const fileDetailsButton = (
|
||||
<button
|
||||
className={`flex items-center gap-1 rounded ${numFilesChanged === 0 ? 'cursor-pointer' : 'cursor-pointer hover:brightness-125 transition-all duration-200'}`}
|
||||
onClick={() => isFileDetailsOpened ? setFileDetailsOpenedState('user-closed') : setFileDetailsOpenedState('user-opened')}
|
||||
type='button'
|
||||
disabled={numFilesChanged === 0}
|
||||
>
|
||||
<svg
|
||||
className="transition-transform duration-200 size-3.5"
|
||||
style={{
|
||||
transform: isFileDetailsOpened ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
transition: 'transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1)'
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="18 15 12 9 6 15"></polyline>
|
||||
</svg>
|
||||
{numFilesChangedStr}
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<SimplifiedToolHeader title={'Changes'}>
|
||||
{sortedCommandBarURIs.map((uri, i) => (
|
||||
<ListableToolItem
|
||||
key={i}
|
||||
name={getBasename(uri.fsPath)}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
|
||||
/>
|
||||
))}
|
||||
</SimplifiedToolHeader>
|
||||
<>
|
||||
{/* file details */}
|
||||
<div className='px-2'>
|
||||
<div
|
||||
className={`
|
||||
select-none
|
||||
flex w-full rounded-t-lg bg-void-bg-3
|
||||
text-void-fg-3 text-xs text-nowrap
|
||||
|
||||
overflow-hidden transition-all duration-200 ease-in-out
|
||||
${isFileDetailsOpened ? 'max-h-24' : 'max-h-0'}
|
||||
`}
|
||||
>
|
||||
{fileDetailsContent}
|
||||
</div>
|
||||
</div>
|
||||
{/* main content */}
|
||||
<div
|
||||
className={`
|
||||
select-none
|
||||
flex w-full rounded-t-lg bg-void-bg-3
|
||||
text-void-fg-3 text-xs text-nowrap
|
||||
border-t border-l border-r border-zinc-300/10
|
||||
|
||||
px-2 py-1
|
||||
justify-between
|
||||
`}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
{fileDetailsButton}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
{acceptRejectAllButtons}
|
||||
{threadStatusHTML}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
|
@ -2004,12 +2355,12 @@ export const SidebarChat = () => {
|
|||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
const isRunning = currThreadStreamState?.isRunning
|
||||
const latestError = currThreadStreamState?.error
|
||||
const messageSoFar = currThreadStreamState?.messageSoFar
|
||||
const displayContentSoFar = currThreadStreamState?.displayContentSoFar
|
||||
const toolCallSoFar = currThreadStreamState?.toolCallSoFar
|
||||
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
|
||||
|
||||
const toolNameSoFar = currThreadStreamState?.toolNameSoFar
|
||||
const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar
|
||||
const toolIsGenerating = !!toolNameSoFar && toolNameSoFar === 'edit_file' // show loading for slow tools (right now just edit)
|
||||
// this is just if it's currently being generated, NOT if it's currently running
|
||||
const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit)
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
|
||||
|
|
@ -2022,8 +2373,6 @@ export const SidebarChat = () => {
|
|||
const sidebarRef = useRef<HTMLDivElement>(null)
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useScrollbarStyles(sidebarRef)
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
|
||||
if (isDisabled) return
|
||||
|
|
@ -2061,11 +2410,10 @@ export const SidebarChat = () => {
|
|||
|
||||
|
||||
const threadId = currentThread.id
|
||||
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity)
|
||||
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity)
|
||||
|
||||
const previousMessagesHTML = useMemo(() => {
|
||||
const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
|
||||
|
||||
// tool request shows up as Editing... if in progress
|
||||
return previousMessages.map((message, i) => {
|
||||
return <ChatBubble
|
||||
|
|
@ -2079,17 +2427,18 @@ export const SidebarChat = () => {
|
|||
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
|
||||
/>
|
||||
})
|
||||
}, [previousMessages, isRunning, threadId])
|
||||
}, [previousMessages, threadId, currCheckpointIdx, isRunning])
|
||||
|
||||
const streamingChatIdx = previousMessagesHTML.length
|
||||
const currStreamingMessageHTML = reasoningSoFar || messageSoFar || isRunning ?
|
||||
const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
|
||||
<ChatBubble
|
||||
key={getChatBubbleId(threadId, streamingChatIdx)}
|
||||
currCheckpointIdx={currCheckpointIdx} // if streaming, can't be the case
|
||||
currCheckpointIdx={currCheckpointIdx}
|
||||
chatMessage={{
|
||||
role: 'assistant',
|
||||
content: messageSoFar ?? '',
|
||||
displayContent: displayContentSoFar ?? '',
|
||||
reasoning: reasoningSoFar ?? '',
|
||||
toolCall: toolCallSoFar,
|
||||
anthropicReasoning: null,
|
||||
}}
|
||||
messageIdx={streamingChatIdx}
|
||||
|
|
@ -2101,7 +2450,14 @@ export const SidebarChat = () => {
|
|||
/> : null
|
||||
|
||||
|
||||
const generatingToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
|
||||
// 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
|
||||
|
|
@ -2112,22 +2468,18 @@ export const SidebarChat = () => {
|
|||
w-full h-full
|
||||
overflow-x-hidden
|
||||
overflow-y-auto
|
||||
${previousMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
|
||||
${previousMessagesHTML.length === 0 && !displayContentSoFar ? 'hidden' : ''}
|
||||
`}
|
||||
>
|
||||
{/* previous messages */}
|
||||
{previousMessagesHTML}
|
||||
|
||||
|
||||
{currStreamingMessageHTML}
|
||||
|
||||
{/* Generating tool */}
|
||||
{generatingTool}
|
||||
|
||||
{toolIsGenerating ?
|
||||
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={generatingToolTitle} desc1={<span className='flex items-center'>Generating<IconLoading /></span>} />
|
||||
: null}
|
||||
|
||||
{/* loading indicator */}
|
||||
{isRunning === 'LLM' && !toolIsGenerating ? <ProseWrapper>
|
||||
{/* loading indicator */}
|
||||
{<IconLoading className='opacity-50 text-sm' />}
|
||||
</ProseWrapper> : null}
|
||||
|
||||
|
|
@ -2159,33 +2511,40 @@ export const SidebarChat = () => {
|
|||
}
|
||||
}, [onSubmit, onAbort, isRunning])
|
||||
|
||||
const inputForm = <div
|
||||
key={'input' + chatThreadsState.currentThreadId}
|
||||
className='px-2 pb-2'>
|
||||
<VoidChatArea
|
||||
featureName='Chat'
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={!!isRunning}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={previousMessagesHTML.length === 0}
|
||||
selections={selections}
|
||||
setSelections={setSelections}
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
const inputForm = <div key={'input' + chatThreadsState.currentThreadId}>
|
||||
<div className='px-4'>
|
||||
{previousMessages.length > 0 &&
|
||||
<CommandBarInChat />
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className='px-2 pb-2'
|
||||
>
|
||||
<VoidInputBox2
|
||||
className={`min-h-[81px] px-0.5 py-0.5`}
|
||||
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
<VoidChatArea
|
||||
featureName='Chat'
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={!!isRunning}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={previousMessagesHTML.length === 0}
|
||||
selections={selections}
|
||||
setSelections={setSelections}
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
>
|
||||
<VoidInputBox2
|
||||
className={`min-h-[81px] px-0.5 py-0.5`}
|
||||
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
|
||||
</VoidChatArea>
|
||||
</VoidChatArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export const SidebarThreadSelector = () => {
|
|||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length;
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
|
||||
return (
|
||||
<textarea
|
||||
autoFocus={false}
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
if (fnsRef)
|
||||
fnsRef.current = fns
|
||||
|
|
@ -153,12 +154,13 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
})
|
||||
|
||||
|
||||
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, ...inputProps }: {
|
||||
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, compact, ...inputProps }: {
|
||||
value: string;
|
||||
onChangeValue: (value: string) => void;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
compact?: boolean;
|
||||
passwordBlur?: boolean;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) => {
|
||||
|
||||
|
|
@ -168,7 +170,11 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam
|
|||
onChange={(e) => onChangeValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
|
||||
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
|
||||
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
|
||||
className={`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
|
||||
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
|
||||
rounded
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
${className}`}
|
||||
style={{
|
||||
|
|
@ -412,6 +418,7 @@ export const VoidSwitch = ({
|
|||
onChange,
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
...props
|
||||
}: {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
|
|
@ -419,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={`
|
||||
|
|
@ -635,7 +642,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
|||
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
<span className={`truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
{getOptionDisplayName(selectedOption)}
|
||||
</span>
|
||||
<svg
|
||||
|
|
@ -954,9 +961,9 @@ export const BlockCode = ({ initValue, language, maxHeight, showScrollbars }: Bl
|
|||
}
|
||||
|
||||
|
||||
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
|
||||
export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: { children: React.ReactNode; disabled?: boolean; onClick: () => void; className?: string }) => {
|
||||
return <button disabled={disabled}
|
||||
className='px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap'
|
||||
className={`px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap flex items-center justify-center ${className || ''}`}
|
||||
onClick={onClick}
|
||||
>{children}</button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -350,9 +352,9 @@ export const useCommandBarURIListener = (listener: (uri: URI) => void) => {
|
|||
export const useCommandBarState = () => {
|
||||
const accessor = useAccessor()
|
||||
const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
const [s, ss] = useState({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
const listener = useCallback(() => {
|
||||
ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
ss({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
}, [commandBarService])
|
||||
useCommandBarURIListener(listener)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,128 +1,130 @@
|
|||
import { useEffect } from 'react';
|
||||
// Get rid of this as it was causing lag
|
||||
|
||||
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
// import { useEffect } from 'react';
|
||||
|
||||
// Create selector for specific overflow classes
|
||||
const overflowSelector = [
|
||||
'[class*="overflow-auto"]',
|
||||
'[class*="overflow-x-auto"]',
|
||||
'[class*="overflow-y-auto"]'
|
||||
].join(',');
|
||||
// export const useScrollbarStyles = (containerRef: React.RefObject<HTMLDivElement | null>) => {
|
||||
// useEffect(() => {
|
||||
// if (!containerRef.current) return;
|
||||
|
||||
// Function to initialize scrollbar styles for elements
|
||||
const initializeScrollbarStyles = () => {
|
||||
// Get all matching elements within the container, including the container itself
|
||||
const scrollElements = [
|
||||
...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
|
||||
];
|
||||
// // Create selector for specific overflow classes
|
||||
// const overflowSelector = [
|
||||
// '[class*="overflow-auto"]',
|
||||
// '[class*="overflow-x-auto"]',
|
||||
// '[class*="overflow-y-auto"]'
|
||||
// ].join(',');
|
||||
|
||||
// Apply basic styling to all elements
|
||||
scrollElements.forEach(element => {
|
||||
element.classList.add('void-scrollable-element');
|
||||
});
|
||||
// // Function to initialize scrollbar styles for elements
|
||||
// const initializeScrollbarStyles = () => {
|
||||
// // Get all matching elements within the container, including the container itself
|
||||
// const scrollElements = [
|
||||
// ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
// ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
|
||||
// ];
|
||||
|
||||
// Only initialize fade effects for elements that haven't been initialized yet
|
||||
scrollElements.forEach(element => {
|
||||
if (!(element as any).__scrollbarCleanup) {
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
// // Apply basic styling to all elements
|
||||
// scrollElements.forEach(element => {
|
||||
// element.classList.add('void-scrollable-element');
|
||||
// });
|
||||
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
// // Only initialize fade effects for elements that haven't been initialized yet
|
||||
// scrollElements.forEach(element => {
|
||||
// if (!(element as any).__scrollbarCleanup) {
|
||||
// let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
// let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
// const fadeIn = () => {
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
// let step = 0;
|
||||
// fadeInterval = setInterval(() => {
|
||||
// if (step <= 10) {
|
||||
// element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
// element.classList.add(`show-scrollbar-${step}`);
|
||||
// step++;
|
||||
// } else {
|
||||
// clearInterval(fadeInterval!);
|
||||
// }
|
||||
// }, 10);
|
||||
// };
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
// const fadeOut = () => {
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
// let step = 10;
|
||||
// fadeInterval = setInterval(() => {
|
||||
// if (step >= 0) {
|
||||
// element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
// element.classList.add(`show-scrollbar-${step}`);
|
||||
// step--;
|
||||
// } else {
|
||||
// clearInterval(fadeInterval!);
|
||||
// }
|
||||
// }, 60);
|
||||
// };
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
// const onMouseEnter = () => {
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
// fadeIn();
|
||||
// };
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
// const onMouseLeave = () => {
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// fadeTimeout = setTimeout(() => {
|
||||
// fadeOut();
|
||||
// }, 10);
|
||||
// };
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
};
|
||||
// element.addEventListener('mouseenter', onMouseEnter);
|
||||
// element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
}
|
||||
});
|
||||
};
|
||||
// // Store cleanup function
|
||||
// const cleanup = () => {
|
||||
// element.removeEventListener('mouseenter', onMouseEnter);
|
||||
// element.removeEventListener('mouseleave', onMouseLeave);
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
// element.classList.remove('void-scrollable-element');
|
||||
// // Remove any remaining show-scrollbar classes
|
||||
// for (let i = 0; i <= 10; i++) {
|
||||
// element.classList.remove(`show-scrollbar-${i}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
// Initialize for the first time
|
||||
initializeScrollbarStyles();
|
||||
// // Store the cleanup function on the element for later use
|
||||
// (element as any).__scrollbarCleanup = cleanup;
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
// Set up mutation observer to do the same
|
||||
const observer = new MutationObserver(() => {
|
||||
initializeScrollbarStyles();
|
||||
});
|
||||
// // Initialize for the first time
|
||||
// initializeScrollbarStyles();
|
||||
|
||||
// Start observing the container for child changes
|
||||
observer.observe(containerRef.current, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
// // Set up mutation observer to do the same
|
||||
// const observer = new MutationObserver(() => {
|
||||
// initializeScrollbarStyles();
|
||||
// });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
// Your existing cleanup code...
|
||||
if (containerRef.current) {
|
||||
const scrollElements = [
|
||||
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
];
|
||||
scrollElements.forEach(element => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [containerRef]);
|
||||
};
|
||||
// // Start observing the container for child changes
|
||||
// observer.observe(containerRef.current, {
|
||||
// childList: true,
|
||||
// subtree: true
|
||||
// });
|
||||
|
||||
// return () => {
|
||||
// observer.disconnect();
|
||||
// // Your existing cleanup code...
|
||||
// if (containerRef.current) {
|
||||
// const scrollElements = [
|
||||
// ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
// ...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
// ];
|
||||
// scrollElements.forEach(element => {
|
||||
// if ((element as any).__scrollbarCleanup) {
|
||||
// (element as any).__scrollbarCleanup();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// }, [containerRef]);
|
||||
// };
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
|
|||
import '../styles.css'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
|
||||
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const isDark = useIsDark()
|
||||
|
|
@ -39,7 +40,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
const commandService = accessor.get('ICommandService')
|
||||
const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
const voidModelService = accessor.get('IVoidModelService')
|
||||
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
|
|
@ -211,38 +212,47 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
|
||||
if (!isADiffZoneInAnyFile) return null
|
||||
|
||||
const acceptAllButton = <button
|
||||
className='text-nowrap'
|
||||
// const acceptAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onAcceptAll}
|
||||
// style={{
|
||||
// backgroundColor: acceptAllBg,
|
||||
// border: acceptBorder,
|
||||
// color: buttonTextColor,
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Accept File
|
||||
// </button>
|
||||
|
||||
// const rejectAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onRejectAll}
|
||||
// style={{
|
||||
// backgroundColor: rejectBg,
|
||||
// border: rejectBorder,
|
||||
// color: 'white',
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Reject File
|
||||
// </button>
|
||||
|
||||
const acceptAllButton = <AcceptAllButtonWrapper
|
||||
text={'Accept File'}
|
||||
onClick={onAcceptAll}
|
||||
style={{
|
||||
backgroundColor: acceptAllBg,
|
||||
border: acceptBorder,
|
||||
color: buttonTextColor,
|
||||
fontSize: buttonFontSize,
|
||||
padding: '2px 4px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Accept File
|
||||
</button>
|
||||
/>
|
||||
|
||||
|
||||
const rejectAllButton = <button
|
||||
className='text-nowrap'
|
||||
const rejectAllButton = <RejectAllButtonWrapper
|
||||
text={'Reject File'}
|
||||
onClick={onRejectAll}
|
||||
style={{
|
||||
backgroundColor: rejectAllBg,
|
||||
border: rejectBorder,
|
||||
color: 'white',
|
||||
fontSize: buttonFontSize,
|
||||
padding: '2px 4px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Reject File
|
||||
</button>
|
||||
/>
|
||||
|
||||
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
|
||||
{acceptAllButton}
|
||||
|
|
@ -273,7 +283,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
|
||||
{upButton}
|
||||
{downButton}
|
||||
<span className="min-w-16 px-2 text-xs">
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{isADiffInThisFile ?
|
||||
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
|
||||
: streamState === 'streaming' ?
|
||||
|
|
@ -289,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
{rightButton}
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
<span className="min-w-16 px-2 text-xs">
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{currFileIdx !== null ?
|
||||
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
|
||||
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
|
||||
|
|
@ -299,7 +309,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
return <div className={`flex flex-col items-center gap-y-2 mx-2 pointer-events-auto`}>
|
||||
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
|
||||
{showAcceptRejectAll && acceptRejectAllButtons}
|
||||
{leftRightUpDownButtons}
|
||||
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { useAccessor, useActiveURI, useIsDark, useSettingsState } from '../util/services.js';
|
||||
|
||||
import '../styles.css'
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { Circle, MoreVertical } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { VoidSelectionHelperProps } from '../../../../../../contrib/void/browser/voidSelectionHelperWidget.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
|
||||
|
||||
export const VoidSelectionHelperMain = (props: VoidSelectionHelperProps) => {
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return <div
|
||||
className={`@@void-scope ${isDark ? 'dark' : ''}`}
|
||||
>
|
||||
<VoidSelectionHelper {...props} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const VoidSelectionHelper = ({ rerenderKey }: VoidSelectionHelperProps) => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const keybindingService = accessor.get('IKeybindingService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const ctrlLKeybind = keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID)
|
||||
const ctrlKKeybind = keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID)
|
||||
|
||||
const dividerHTML = <div className='w-[0.5px] bg-void-border-3'></div>
|
||||
|
||||
const [reactRerenderCount, setReactRerenderKey] = useState(rerenderKey)
|
||||
const [clickState, setClickState] = useState<'init' | 'clickedOption' | 'clickedMore'>('init')
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = commandService.onWillExecuteCommand(e => {
|
||||
if (e.commandId === VOID_CTRL_L_ACTION_ID || e.commandId === VOID_CTRL_K_ACTION_ID) {
|
||||
setClickState('clickedOption')
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [commandService, setClickState]);
|
||||
|
||||
|
||||
// rerender when the key changes
|
||||
if (reactRerenderCount !== rerenderKey) {
|
||||
setReactRerenderKey(rerenderKey)
|
||||
setClickState('init')
|
||||
}
|
||||
// useEffect(() => {
|
||||
// }, [rerenderKey, reactRerenderCount, setReactRerenderKey, setClickState])
|
||||
|
||||
// if the user selected an option, close
|
||||
|
||||
|
||||
if (clickState === 'clickedOption') {
|
||||
return null
|
||||
}
|
||||
|
||||
const defaultHTML = <>
|
||||
{ctrlLKeybind &&
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
<span>Add to Chat</span>
|
||||
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
|
||||
{ctrlLKeybind.getLabel()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
{ctrlLKeybind && ctrlKKeybind &&
|
||||
dividerHTML
|
||||
}
|
||||
{ctrlKKeybind &&
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_CTRL_K_ACTION_ID)
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
<span className='ml-1'>Edit Inline</span>
|
||||
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
|
||||
{ctrlKKeybind.getLabel()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
{dividerHTML}
|
||||
|
||||
<div
|
||||
className='
|
||||
flex items-center px-0.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
setClickState('clickedMore');
|
||||
}}
|
||||
>
|
||||
<MoreVertical className="w-4" />
|
||||
</div>
|
||||
</>
|
||||
|
||||
|
||||
const moreOptionsHTML = <>
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
Disable Suggestions?
|
||||
</div>
|
||||
|
||||
{dividerHTML}
|
||||
|
||||
<div
|
||||
className='
|
||||
flex items-center px-0.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
setClickState('init');
|
||||
}}
|
||||
>
|
||||
<MoreVertical className="w-4" />
|
||||
</div>
|
||||
</>
|
||||
|
||||
return <div className='
|
||||
pointer-events-auto select-none
|
||||
z-[1000]
|
||||
rounded-sm shadow-md flex flex-nowrap text-nowrap
|
||||
border border-void-border-3 bg-void-bg-2
|
||||
transition-all duration-200
|
||||
'>
|
||||
{clickState === 'init' ? defaultHTML
|
||||
: clickState === 'clickedMore' ? moreOptionsHTML
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -5,5 +5,9 @@
|
|||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { VoidCommandBarMain } from './VoidCommandBar.js'
|
||||
import { VoidSelectionHelperMain } from './VoidSelectionHelper.js'
|
||||
|
||||
export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain)
|
||||
|
||||
export const mountVoidSelectionHelper = mountFnGenerator(VoidSelectionHelperMain)
|
||||
|
||||
|
|
@ -0,0 +1,745 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
|
||||
export const VoidOnboarding = () => {
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return (
|
||||
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div
|
||||
className={`
|
||||
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,26 +3,24 @@
|
|||
* 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, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName } 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 { VoidButton, 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 } from 'lucide-react'
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
|
||||
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 { IconX } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
|
||||
|
||||
|
||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||
|
||||
return <div className='flex items-center text-void-fg-3 px-3 py-0.5 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
return <div className='flex items-center text-void-fg-3 px-3 py-0.5 rounded-sm overflow-hidden gap-2'>
|
||||
{leftButton ? leftButton : null}
|
||||
<span>
|
||||
{text}
|
||||
|
|
@ -98,58 +96,136 @@ const RefreshableModels = () => {
|
|||
|
||||
|
||||
|
||||
const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: () => void }) => {
|
||||
export const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => {
|
||||
const [dashOffset, setDashOffset] = useState(40);
|
||||
|
||||
useEffect(() => {
|
||||
const startTime = performance.now();
|
||||
const duration = 500; // 500ms animation
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const newOffset = 40 - (progress * 40);
|
||||
|
||||
setDashOffset(newOffset);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
const animationId = requestAnimationFrame(animate);
|
||||
return () => cancelAnimationFrame(animationId);
|
||||
}, []);
|
||||
|
||||
return <div
|
||||
className={`flex items-center gap-1.5 w-fit
|
||||
${className ? className : `px-2 py-0.5 text-xs text-white bg-[#0e70c0] rounded-sm`}
|
||||
`}
|
||||
>
|
||||
<svg className="size-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5 13l4 4L19 7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
strokeDasharray: 40,
|
||||
strokeDashoffset: dashOffset
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
{text}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, text?: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
disabled={disabled}
|
||||
className={`bg-[#0e70c0] px-3 py-1 text-white rounded-sm ${!disabled ? 'hover:bg-[#1177cb] cursor-pointer' : 'opacity-50 cursor-not-allowed bg-opacity-70'}`}
|
||||
{...props}
|
||||
>{text}</button>
|
||||
|
||||
}
|
||||
|
||||
|
||||
// shows a providerName dropdown if no `providerName` is given
|
||||
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showCheckmark, setShowCheckmark] = useState(false)
|
||||
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [providerName, setProviderName] = useState<ProviderName | null>(null)
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
|
||||
|
||||
const modelNameRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const providerName = permanentProviderName ?? userChosenProviderName;
|
||||
|
||||
const [modelName, setModelName] = useState<string>('')
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
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}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
|
||||
>
|
||||
<div>
|
||||
{numModels > 0 ? `Add a different model?` : `Add a model`}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
return <>
|
||||
<div className='flex items-center gap-4'>
|
||||
<form className={`flex items-center gap-2 ${className}`}>
|
||||
|
||||
{/* provider */}
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
|
||||
py-[4px] px-[6px]
|
||||
`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
{/* <_VoidSelectBox
|
||||
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
|
||||
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
|
||||
options={providerOptions}
|
||||
/> */}
|
||||
{/* X button
|
||||
<button onClick={() => { setIsOpen(false) }} className='text-void-fg-4'><X className='size-4' /></button> */}
|
||||
|
||||
{/* model */}
|
||||
<div className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'>
|
||||
<VoidInputBox2
|
||||
placeholder='Model Name'
|
||||
className='mt-[2px] px-[6px] h-full w-full'
|
||||
ref={modelNameRef}
|
||||
multiline={false}
|
||||
{/* provider input */}
|
||||
{!permanentProviderName &&
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setUserChosenProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
|
||||
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* button */}
|
||||
<VoidButton
|
||||
onClick={() => {
|
||||
const modelName = modelNameRef.current?.value
|
||||
{/* model input */}
|
||||
<VoidSimpleInputBox
|
||||
value={modelName}
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
compact={compact}
|
||||
className={'max-w-32'}
|
||||
/>
|
||||
|
||||
{/* add button */}
|
||||
<AddButton
|
||||
type='submit'
|
||||
disabled={!modelName}
|
||||
onClick={(e) => {
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
|
|
@ -160,17 +236,24 @@ const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: ()
|
|||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists under ${providerName}.`)
|
||||
// setErrorString(`This model already exists under ${providerName}.`)
|
||||
setErrorString(`This model already exists.`)
|
||||
return
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
onSubmit()
|
||||
setShowCheckmark(true)
|
||||
setTimeout(() => {
|
||||
setShowCheckmark(false)
|
||||
setIsOpen(false)
|
||||
}, 1500)
|
||||
setErrorString('')
|
||||
setModelName('')
|
||||
}}
|
||||
>Add model</VoidButton>
|
||||
/>
|
||||
|
||||
<button onClick={onClose} className='ml-auto'><X className='size-4' /></button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap mt-1'>
|
||||
{errorString}
|
||||
|
|
@ -180,17 +263,6 @@ const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: ()
|
|||
|
||||
}
|
||||
|
||||
const AddModelMenuFull = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 px-3 rounded-sm overflow-hidden'>
|
||||
{open ?
|
||||
<AddModelMenu onSubmit={() => setOpen(false)} onClose={() => setOpen(false)} />
|
||||
: <VoidButton onClick={() => setOpen(true)}>Add Model</VoidButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
export const ModelDump = () => {
|
||||
|
||||
|
|
@ -200,7 +272,7 @@ export const ModelDump = () => {
|
|||
const settingsState = useSettingsState()
|
||||
|
||||
// a dump of all the enabled providers' models
|
||||
const modelDump: (VoidModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
const modelDump: (VoidStatefulModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
for (let providerName of providerNames) {
|
||||
const providerSettings = settingsState.settingsOfProvider[providerName]
|
||||
// if (!providerSettings.enabled) continue
|
||||
|
|
@ -218,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
|
||||
|
|
@ -226,18 +307,29 @@ 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`}>
|
||||
|
|
@ -277,6 +369,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
passwordBlur={isPasswordField}
|
||||
compact={true}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender string={subTextMd} chatMessageLocation={undefined} />
|
||||
|
|
@ -286,7 +379,52 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
// const OldSettingsForProvider = ({ providerName, showProviderTitle }: { providerName: ProviderName, showProviderTitle: boolean }) => {
|
||||
// const voidSettingsState = useSettingsState()
|
||||
|
||||
// const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
|
||||
|
||||
// // const accessor = useAccessor()
|
||||
// // const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// // const { enabled } = voidSettingsState.settingsOfProvider[providerName]
|
||||
// const settingNames = customSettingNamesOfProvider(providerName)
|
||||
|
||||
// const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
|
||||
// return <div className='my-4'>
|
||||
|
||||
// <div className='flex items-center w-full gap-4'>
|
||||
// {showProviderTitle && <h3 className='text-xl truncate'>{providerTitle}</h3>}
|
||||
|
||||
// {/* enable provider switch */}
|
||||
// {/* <VoidSwitch
|
||||
// value={!!enabled}
|
||||
// onChange={
|
||||
// useCallback(() => {
|
||||
// const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled
|
||||
// voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
|
||||
// }, [voidSettingsService, providerName])}
|
||||
// size='sm+'
|
||||
// /> */}
|
||||
// </div>
|
||||
|
||||
// <div className='px-0'>
|
||||
// {/* settings besides models (e.g. api key) */}
|
||||
// {settingNames.map((settingName, i) => {
|
||||
// return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
// })}
|
||||
|
||||
// {needsModel ?
|
||||
// providerName === 'ollama' ?
|
||||
// <WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
// : <WarningBox text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
// : null}
|
||||
// </div>
|
||||
// </div >
|
||||
// }
|
||||
|
||||
export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
|
||||
|
|
@ -299,10 +437,10 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
|||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
|
||||
return <div className='my-4'>
|
||||
return <div>
|
||||
|
||||
<div className='flex items-center w-full gap-4'>
|
||||
<h3 className='text-xl truncate'>{providerTitle}</h3>
|
||||
{showProviderTitle && <h3 className='text-xl truncate'>{providerTitle}</h3>}
|
||||
|
||||
{/* enable provider switch */}
|
||||
{/* <VoidSwitch
|
||||
|
|
@ -322,7 +460,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
|||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
})}
|
||||
|
||||
{needsModel ?
|
||||
{showProviderSuggestions && needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
|
|
@ -335,7 +473,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
|||
export const VoidProviderSettings = ({ providerNames }: { providerNames: ProviderName[] }) => {
|
||||
return <>
|
||||
{providerNames.map(providerName =>
|
||||
<SettingsForProvider key={providerName} providerName={providerName} />
|
||||
<SettingsForProvider key={providerName} providerName={providerName} showProviderTitle={true} showProviderSuggestions={true} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
|
@ -408,6 +546,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()
|
||||
|
|
@ -418,7 +579,8 @@ export const FeaturesTab = () => {
|
|||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
<AddModelInputBox className='mt-4' compact />
|
||||
<RedoOnboardingButton className='mt-2 mb-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
|
|
@ -429,13 +591,9 @@ export const FeaturesTab = () => {
|
|||
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className='pl-4 prose-ol:list-decimal opacity-80'>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></span>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
{ollamaSetupInstructions}
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
|
|
@ -530,12 +688,39 @@ 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 ? 'Include after-edit lint errors' : `Don't include lint errors`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -547,41 +732,91 @@ export const FeaturesTab = () => {
|
|||
}
|
||||
|
||||
|
||||
|
||||
type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
|
||||
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
|
||||
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
|
||||
type TransferFilesInfo = { from: URI, to: URI }[]
|
||||
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): TransferFilesInfo => {
|
||||
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType = 'VS Code'): TransferFilesInfo => {
|
||||
if (os === null)
|
||||
throw new Error(`One-click switch is not possible in this environment.`)
|
||||
if (os === 'mac') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`$HOME not found`)
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
if (os === 'linux') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`variable for $HOME location not found`)
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
if (os === 'windows') {
|
||||
|
|
@ -590,73 +825,115 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): Transfe
|
|||
const userprofile = env['USERPROFILE']
|
||||
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
|
||||
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`os '${os}' not recognized`)
|
||||
throw new Error(`os '${os}' not recognized or editor type '${fromEditor}' not supported for this OS`)
|
||||
}
|
||||
|
||||
|
||||
let transferTheseFiles: TransferFilesInfo = []
|
||||
let transferError: string | null = null
|
||||
|
||||
try { transferTheseFiles = transferTheseFilesOfOS(os) }
|
||||
catch (e) { transferError = e + '' }
|
||||
|
||||
const OneClickSwitchButton = () => {
|
||||
export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const fileService = accessor.get('IFileService')
|
||||
|
||||
const [state, setState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' })
|
||||
const [transferState, setTransferState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' })
|
||||
|
||||
let transferTheseFiles: TransferFilesInfo = [];
|
||||
let editorError: string | null = null;
|
||||
|
||||
try {
|
||||
transferTheseFiles = transferTheseFilesOfOS(os, fromEditor)
|
||||
} catch (e) {
|
||||
editorError = e + ''
|
||||
}
|
||||
|
||||
if (transferTheseFiles.length === 0)
|
||||
return <>
|
||||
<WarningBox text={transferError ?? `One-click switch not available.`} />
|
||||
<WarningBox text={editorError ?? `Transfer from ${fromEditor} not available.`} />
|
||||
</>
|
||||
|
||||
|
||||
|
||||
const onClick = async () => {
|
||||
if (transferState.type !== 'done') return
|
||||
|
||||
if (state.type !== 'done') return
|
||||
|
||||
setState({ type: 'loading' })
|
||||
setTransferState({ type: 'loading' })
|
||||
|
||||
let errAcc = ''
|
||||
for (let { from, to } of transferTheseFiles) {
|
||||
console.log('transferring', from, to)
|
||||
// not sure if this can fail, just wrapping it with try/catch for now
|
||||
try { await fileService.copy(from, to, true) }
|
||||
catch (e) { errAcc += e + '\n' }
|
||||
// Check if the source file exists before attempting to copy
|
||||
try {
|
||||
const exists = await fileService.exists(from)
|
||||
if (exists) {
|
||||
// Ensure the destination directory exists
|
||||
const toParent = URI.joinPath(to, '..')
|
||||
const toParentExists = await fileService.exists(toParent)
|
||||
if (!toParentExists) {
|
||||
await fileService.createFolder(toParent)
|
||||
}
|
||||
await fileService.copy(from, to, true)
|
||||
} else {
|
||||
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Error copying file:', e)
|
||||
errAcc += `Error copying ${from.toString()}: ${e}\n`
|
||||
}
|
||||
}
|
||||
|
||||
// Even if some files were missing, consider it a success if no actual errors occurred
|
||||
const hadError = !!errAcc
|
||||
if (hadError) {
|
||||
setState({ type: 'done', error: errAcc })
|
||||
setTransferState({ type: 'done', error: errAcc })
|
||||
}
|
||||
else {
|
||||
setState({ type: 'justfinished' })
|
||||
setTimeout(() => { setState({ type: 'done' }); }, 3000)
|
||||
setTransferState({ type: 'justfinished' })
|
||||
setTimeout(() => { setTransferState({ type: 'done' }); }, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<VoidButton disabled={state.type !== 'done'} onClick={onClick}>
|
||||
{state.type === 'done' ? 'Transfer my Settings'
|
||||
: state.type === 'loading' ? 'Transferring...'
|
||||
: state.type === 'justfinished' ? 'Success!'
|
||||
<VoidButtonBgDarken className={`max-w-48 p-4 ${className}`} disabled={transferState.type !== 'done'} onClick={onClick}>
|
||||
{transferState.type === 'done' ? `Transfer from ${fromEditor}`
|
||||
: transferState.type === 'loading' ? <span className='text-nowrap flex flex-nowrap'>Transferring<IconLoading /></span>
|
||||
: transferState.type === 'justfinished' ? <AnimatedCheckmarkButton text='Settings Transferred' className='bg-none' />
|
||||
: null
|
||||
}
|
||||
</VoidButton>
|
||||
{state.type === 'done' && state.error ? <WarningBox text={state.error} /> : null}
|
||||
</VoidButtonBgDarken>
|
||||
{transferState.type === 'done' && transferState.error ? <WarningBox text={transferState.error} /> : null}
|
||||
</>
|
||||
}
|
||||
|
||||
|
|
@ -668,46 +945,49 @@ const GeneralTab = () => {
|
|||
const nativeHostService = accessor.get('INativeHostService')
|
||||
|
||||
return <>
|
||||
|
||||
|
||||
<div className=''>
|
||||
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Transfer your settings from VS Code to Void in one click.`}</h4>
|
||||
<OneClickSwitchButton />
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-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'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButton>
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButton>
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButton>
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButton>
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
<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>
|
||||
|
||||
|
|
@ -722,11 +1002,8 @@ export const Settings = () => {
|
|||
|
||||
const [tab, setTab] = useState<TabName>('models')
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
useScrollbarStyles(containerRef)
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div ref={containerRef} className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
|
||||
<div className='max-w-5xl mx-auto'>
|
||||
|
||||
|
|
@ -770,3 +1047,4 @@ export const Settings = () => {
|
|||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
import { useIsDark } from '../util/services.js';
|
||||
|
||||
/**
|
||||
* Creates a configured global tooltip component with consistent styling
|
||||
* To use:
|
||||
* 1. Mount a Tooltip with some id eg id='void-tooltip'
|
||||
* 2. Add data-tooltip-id="void-tooltip" and data-tooltip-content="Your tooltip text" to any element
|
||||
*/
|
||||
export const VoidTooltip = () => {
|
||||
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return (
|
||||
|
||||
// use native colors so we don't have to worry about @@void-scope styles
|
||||
// --void-bg-1: var(--vscode-input-background);
|
||||
// --void-bg-1-alt: var(--vscode-badge-background);
|
||||
// --void-bg-2: var(--vscode-sideBar-background);
|
||||
// --void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
|
||||
// --void-bg-3: var(--vscode-editor-background);
|
||||
|
||||
// --void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
||||
// --void-fg-1: var(--vscode-editor-foreground);
|
||||
// --void-fg-2: var(--vscode-input-foreground);
|
||||
// --void-fg-3: var(--vscode-input-placeholderForeground);
|
||||
// /* --void-fg-4: var(--vscode-tab-inactiveForeground); */
|
||||
// --void-fg-4: var(--vscode-list-deemphasizedForeground);
|
||||
|
||||
// --void-warning: var(--vscode-charts-yellow);
|
||||
|
||||
// --void-border-1: var(--vscode-commandCenter-activeBorder);
|
||||
// --void-border-2: var(--vscode-commandCenter-border);
|
||||
// --void-border-3: var(--vscode-commandCenter-inactiveBorder);
|
||||
// --void-border-4: var(--vscode-editorGroup-border);
|
||||
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
#void-tooltip, #void-tooltip-orange, #void-tooltip-green {
|
||||
font-size: 12px;
|
||||
padding: 0px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
#void-tooltip {
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
}
|
||||
|
||||
#void-tooltip-orange {
|
||||
background-color: #F6762A;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#void-tooltip-green {
|
||||
background-color: #228B22;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.react-tooltip-arrow {
|
||||
z-index: -1 !important; /* Keep arrow behind content (somehow this isnt done automatically) */
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
|
||||
<Tooltip
|
||||
id="void-tooltip"
|
||||
// border='1px solid var(--vscode-editorGroup-border)'
|
||||
border='1px solid rgba(100,100,100,.2)'
|
||||
opacity={1}
|
||||
delayShow={50}
|
||||
/>
|
||||
<Tooltip
|
||||
id="void-tooltip-orange"
|
||||
border='1px solid rgba(200,200,200,.3)'
|
||||
opacity={1}
|
||||
delayShow={50}
|
||||
/>
|
||||
<Tooltip
|
||||
id="void-tooltip-green"
|
||||
border='1px solid rgba(200,200,200,.3)'
|
||||
opacity={1}
|
||||
delayShow={50}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 { VoidTooltip } from './VoidTooltip.js'
|
||||
|
||||
export const mountVoidTooltip = mountFnGenerator(VoidTooltip)
|
||||
|
|
@ -7,9 +7,11 @@ import { defineConfig } from 'tsup'
|
|||
|
||||
export default defineConfig({
|
||||
entry: [
|
||||
'./src2/void-command-bar-tsx/index.tsx',
|
||||
'./src2/void-editor-widgets-tsx/index.tsx',
|
||||
'./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',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ registerAction2(class extends Action2 {
|
|||
super({
|
||||
id: VOID_CTRL_L_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
|
||||
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID
|
|||
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
const container = viewContainerRegistry.registerViewContainer({
|
||||
id: VOID_VIEW_CONTAINER_ID,
|
||||
title: nls.localize2('voidContainer', 'Void Chat'), // this is used to say "Void" (Ctrl + L)
|
||||
title: nls.localize2('voidContainer', 'Chat'), // this is used to say "Void" (Ctrl + L)
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, {
|
||||
mergeViewWithContainerWhenSingleView: true,
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
|
|||
import { ISearchService } from '../../../services/search/common/search.js'
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js'
|
||||
import { ITerminalToolService } from './terminalToolService.js'
|
||||
import { ToolCallParams, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { IVoidModelService } from '../common/voidModelService.js'
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
import { basename } from '../../../../base/common/path.js'
|
||||
|
|
@ -16,6 +16,9 @@ import { IVoidCommandBarService } from './voidCommandBarService.js'
|
|||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
|
||||
import { IMarkerService } 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
|
||||
|
|
@ -23,7 +26,7 @@ import { timeout } from '../../../../base/common/async.js'
|
|||
|
||||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: string) => 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 }
|
||||
|
||||
|
|
@ -34,35 +37,16 @@ type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awai
|
|||
export const MAX_FILE_CHARS_PAGE = 50_000
|
||||
export const MAX_CHILDREN_URIs_PAGE = 500
|
||||
export const MAX_TERMINAL_CHARS_PAGE = 20_000
|
||||
export const TERMINAL_TIMEOUT_TIME = 15
|
||||
export const TERMINAL_TIMEOUT_TIME = 5 // seconds
|
||||
export const TERMINAL_BG_WAIT_TIME = 1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const validateJSON = (s: string): { [s: string]: unknown } => {
|
||||
try {
|
||||
const o = JSON.parse(s)
|
||||
if (typeof o !== 'object') throw new Error()
|
||||
|
||||
if ('result' in o) { // openrouter sometimes wraps the result with { 'result': ... }
|
||||
return o.result
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Invalid LLM output format: Tool parameter was not a string of a valid JSON: "${s}".`)
|
||||
}
|
||||
}
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
return !u || u === 'null' || u === 'undefined'
|
||||
}
|
||||
|
||||
const validateStr = (argName: string, value: unknown) => {
|
||||
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`)
|
||||
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a ${typeof value}. Value: ${value}.`)
|
||||
return value
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +54,7 @@ const validateStr = (argName: string, value: unknown) => {
|
|||
// We are NOT checking to make sure in workspace
|
||||
// TODO!!!! check to make sure folder/file exists
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.')
|
||||
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a ${typeof uriStr}. Value: ${uriStr}.`)
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
|
@ -109,6 +93,7 @@ const validateNumber = (numStr: unknown, opts: { default: number | null }) => {
|
|||
}
|
||||
|
||||
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
|
||||
if (!paramsUnknown) return false
|
||||
if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.')
|
||||
const params = paramsUnknown
|
||||
const isRecursive = params.includes('r')
|
||||
|
|
@ -167,15 +152,14 @@ 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: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -184,43 +168,39 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
return { uri, startLine, endLine, pageNumber }
|
||||
},
|
||||
ls_dir: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
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: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, } = o
|
||||
get_dir_structure: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, } = params
|
||||
const uri = validateURI(uriStr)
|
||||
return { rootURI: uri }
|
||||
},
|
||||
search_pathnames_only: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
search_pathnames_only: (params: RawToolParamsObj) => {
|
||||
const {
|
||||
query: queryUnknown,
|
||||
include: includeUnknown,
|
||||
pageNumber: pageNumberUnknown
|
||||
} = o
|
||||
search_in_folder: includeUnknown,
|
||||
page_number: pageNumberUnknown
|
||||
} = params
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
const include = validateOptionalStr('include', includeUnknown)
|
||||
const searchInFolder = validateOptionalStr('search_in_folder', includeUnknown)
|
||||
|
||||
return { queryStr, include, pageNumber }
|
||||
return { queryStr, searchInFolder, pageNumber }
|
||||
|
||||
},
|
||||
search_files: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
search_files: (params: RawToolParamsObj) => {
|
||||
const {
|
||||
query: queryUnknown,
|
||||
searchInFolder: searchInFolderUnknown,
|
||||
isRegex: isRegexUnknown,
|
||||
pageNumber: pageNumberUnknown
|
||||
} = o
|
||||
search_in_folder: searchInFolderUnknown,
|
||||
is_regex: isRegexUnknown,
|
||||
page_number: pageNumberUnknown
|
||||
} = params
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
|
@ -231,20 +211,26 @@ 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: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriUnknown } = o
|
||||
create_file_or_folder: (params: RawToolParamsObj) => {
|
||||
const { uri: uriUnknown } = params
|
||||
const uri = validateURI(uriUnknown)
|
||||
const uriStr = validateStr('uri', uriUnknown)
|
||||
const isFolder = checkIfIsFolder(uriStr)
|
||||
return { uri, isFolder }
|
||||
},
|
||||
|
||||
delete_file_or_folder: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriUnknown, params: paramsStr } = o
|
||||
delete_file_or_folder: (params: RawToolParamsObj) => {
|
||||
const { uri: uriUnknown, params: paramsStr } = params
|
||||
const uri = validateURI(uriUnknown)
|
||||
const isRecursive = validateRecursiveParamStr(paramsStr)
|
||||
const uriStr = validateStr('uri', uriUnknown)
|
||||
|
|
@ -252,17 +238,15 @@ export class ToolsService implements IToolsService {
|
|||
return { uri, isRecursive, isFolder }
|
||||
},
|
||||
|
||||
edit_file: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
|
||||
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 (s: string) => {
|
||||
const o = validateJSON(s)
|
||||
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
|
||||
run_terminal_command: (params: RawToolParamsObj) => {
|
||||
const { command: commandUnknown, terminal_id: terminalIdUnknown, wait_for_completion: waitForCompletionUnknown } = params
|
||||
const command = validateStr('command', commandUnknown)
|
||||
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
|
||||
const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true })
|
||||
|
|
@ -302,17 +286,15 @@ export class ToolsService implements IToolsService {
|
|||
},
|
||||
|
||||
get_dir_structure: async ({ rootURI }) => {
|
||||
const result = await this.directoryStrService.getDirectoryStrTool(rootURI)
|
||||
let str = result.str
|
||||
if (result.wasCutOff) str += '\n(Result was truncated)'
|
||||
const str = await this.directoryStrService.getDirectoryStrTool(rootURI)
|
||||
return { result: { str } }
|
||||
},
|
||||
|
||||
search_pathnames_only: async ({ queryStr, include, pageNumber }) => {
|
||||
search_pathnames_only: async ({ queryStr, searchInFolder, pageNumber }) => {
|
||||
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
|
||||
filePattern: queryStr,
|
||||
includePattern: include ?? undefined,
|
||||
includePattern: searchInFolder ?? undefined,
|
||||
})
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
|
|
@ -348,6 +330,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 }) => {
|
||||
|
|
@ -385,15 +373,11 @@ export class ToolsService implements IToolsService {
|
|||
editCodeService.interruptURIStreaming({ uri: diffZoneURI })
|
||||
}
|
||||
|
||||
// at end, get lint errors
|
||||
const lintErrorsPromise = applyDonePromise.then(async () => {
|
||||
await timeout(500)
|
||||
const lintErrorsStr = this.markerService
|
||||
.read({ resource: uri })
|
||||
.map(l => l.message)
|
||||
.join('\n')
|
||||
|
||||
if (!lintErrorsStr) return { lintErrorsStr: null }
|
||||
return { lintErrorsStr }
|
||||
await timeout(2000)
|
||||
const { lintErrors } = this._getLintErrors(uri)
|
||||
return { lintErrors }
|
||||
})
|
||||
|
||||
return { result: lintErrorsPromise, interruptTool }
|
||||
|
|
@ -407,6 +391,12 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
|
||||
|
||||
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) => {
|
||||
|
|
@ -425,6 +415,11 @@ export class ToolsService implements IToolsService {
|
|||
search_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.`
|
||||
|
|
@ -433,8 +428,13 @@ export class ToolsService implements IToolsService {
|
|||
return `URI ${params.uri.fsPath} successfully deleted.`
|
||||
},
|
||||
edit_file: (params, result) => {
|
||||
const additionalStr = result.lintErrorsStr ? `Lint errors found after change:\n${result.lintErrorsStr}.\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}`
|
||||
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.`)
|
||||
: '')
|
||||
|
||||
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
|
||||
},
|
||||
run_terminal_command: (params, result) => {
|
||||
const {
|
||||
|
|
@ -447,7 +447,7 @@ export class ToolsService implements IToolsService {
|
|||
const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}`
|
||||
|
||||
if (resolveReason.type === 'timeout') {
|
||||
return `Terminal command ran in ${terminalDesc}, but timed out after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
|
||||
return `Terminal command ran in ${terminalDesc}, but did not complete after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
|
||||
}
|
||||
else if (resolveReason.type === 'bgtask') {
|
||||
return `Terminal command is running in the background in ${terminalDesc}. Here were the outputs after ${TERMINAL_BG_WAIT_TIME} seconds:\n${result_}`
|
||||
|
|
@ -468,6 +468,21 @@ export class ToolsService implements IToolsService {
|
|||
}
|
||||
|
||||
|
||||
private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } {
|
||||
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, }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
|
||||
|
|
|
|||
55
src/vs/workbench/contrib/void/browser/tooltipService.ts
Normal file
55
src/vs/workbench/contrib/void/browser/tooltipService.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { mountVoidTooltip } from './react/out/void-tooltip/index.js';
|
||||
import { h, getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
|
||||
// Tooltip contribution that mounts the component at startup
|
||||
export class TooltipContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.voidTooltip';
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
this.initializeTooltip();
|
||||
}
|
||||
|
||||
private initializeTooltip(): 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) {
|
||||
// Create a container element for the tooltip using h function
|
||||
const tooltipContainer = h('div.void-tooltip-container').root;
|
||||
workbench.appendChild(tooltipContainer);
|
||||
|
||||
// Mount the React component
|
||||
this.instantiationService.invokeFunction((accessor: ServicesAccessor) => {
|
||||
const result = mountVoidTooltip(tooltipContainer, accessor);
|
||||
if (result && typeof result.dispose === 'function') {
|
||||
this._register(toDisposable(result.dispose));
|
||||
}
|
||||
});
|
||||
|
||||
// Register cleanup for the DOM element
|
||||
this._register(toDisposable(() => {
|
||||
if (tooltipContainer.parentElement) {
|
||||
tooltipContainer.parentElement.removeChild(tooltipContainer);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the contribution to be initialized during the AfterRestored phase
|
||||
registerWorkbenchContribution2(TooltipContribution.ID, TooltipContribution, WorkbenchPhase.AfterRestored);
|
||||
|
|
@ -46,6 +46,15 @@ import './metricsPollService.js'
|
|||
// helper services
|
||||
import './helperServices/consistentItemService.js'
|
||||
|
||||
// register selection helper
|
||||
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
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Widget } from '../../../../base/browser/ui/widget.js';
|
|||
import { IOverlayWidget, ICodeEditor, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js'
|
||||
import { mountVoidCommandBar } from './react/out/void-editor-widgets-tsx/index.js'
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
|
||||
// Mount the React component
|
||||
this.instantiationService.invokeFunction((accessor: ServicesAccessor) => {
|
||||
const result = mountVoidOnboarding(onboardingContainer, accessor);
|
||||
if (result && typeof result.dispose === 'function') {
|
||||
this._register(toDisposable(result.dispose));
|
||||
}
|
||||
});
|
||||
|
||||
// Register cleanup for the DOM element
|
||||
this._register(toDisposable(() => {
|
||||
if (onboardingContainer.parentElement) {
|
||||
onboardingContainer.parentElement.removeChild(onboardingContainer);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the contribution to be initialized during the AfterRestored phase
|
||||
registerWorkbenchContribution2(OnboardingContribution.ID, OnboardingContribution, WorkbenchPhase.AfterRestored);
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { ICursorSelectionChangedEvent } from '../../../../editor/common/cursorEvents.js';
|
||||
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
|
||||
import { Selection } from '../../../../editor/common/core/selection.js';
|
||||
import { RunOnceScheduler } from '../../../../base/common/async.js';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
import { mountVoidSelectionHelper } from './react/out/void-editor-widgets-tsx/index.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
|
||||
import { getLengthOfTextPx } from './editCodeService.js';
|
||||
|
||||
|
||||
const minDistanceFromRightPx = 400;
|
||||
const minLeftPx = 60;
|
||||
|
||||
|
||||
export type VoidSelectionHelperProps = {
|
||||
rerenderKey: number // alternates between 0 and 1
|
||||
}
|
||||
|
||||
|
||||
export class SelectionHelperContribution extends Disposable implements IEditorContribution, IOverlayWidget {
|
||||
public static readonly ID = 'editor.contrib.voidSelectionHelper';
|
||||
// react
|
||||
private _rootHTML: HTMLElement;
|
||||
private _rerender: (props?: any) => void = () => { };
|
||||
private _rerenderKey: number = 0;
|
||||
private _reactComponentDisposable: IDisposable | null = null;
|
||||
|
||||
// internal
|
||||
private _isVisible = false;
|
||||
private _showScheduler: RunOnceScheduler;
|
||||
private _lastSelection: Selection | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IVoidSettingsService private readonly _voidSettingsService: IVoidSettingsService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Create the container element for React component
|
||||
const { root, content } = dom.h('div@root', [
|
||||
dom.h('div@content', [])
|
||||
]);
|
||||
|
||||
// Set styles for container
|
||||
root.style.position = 'absolute';
|
||||
root.style.display = 'none'; // Start hidden
|
||||
root.style.pointerEvents = 'none';
|
||||
root.style.marginLeft = '16px';
|
||||
|
||||
// Initialize React component
|
||||
this._instantiationService.invokeFunction(accessor => {
|
||||
if (this._reactComponentDisposable) {
|
||||
this._reactComponentDisposable.dispose();
|
||||
}
|
||||
const res = mountVoidSelectionHelper(content, accessor);
|
||||
if (!res) return;
|
||||
|
||||
this._reactComponentDisposable = res;
|
||||
this._rerender = res.rerender;
|
||||
|
||||
this._register(this._reactComponentDisposable);
|
||||
|
||||
|
||||
});
|
||||
|
||||
this._rootHTML = root;
|
||||
|
||||
// Register as overlay widget
|
||||
this._editor.addOverlayWidget(this);
|
||||
|
||||
// Use scheduler to debounce showing widget
|
||||
this._showScheduler = new RunOnceScheduler(() => {
|
||||
if (this._lastSelection) {
|
||||
this._showHelperForSelection(this._lastSelection);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// Register event listeners
|
||||
this._register(this._editor.onDidChangeCursorSelection(e => this._onSelectionChange(e)));
|
||||
|
||||
// Add a flag to track if mouse is over the widget
|
||||
let isMouseOverWidget = false;
|
||||
this._rootHTML.addEventListener('mouseenter', () => {
|
||||
isMouseOverWidget = true;
|
||||
});
|
||||
this._rootHTML.addEventListener('mouseleave', () => {
|
||||
isMouseOverWidget = false;
|
||||
});
|
||||
|
||||
// Only hide helper when text editor loses focus and mouse is not over the widget
|
||||
this._register(this._editor.onDidBlurEditorText(() => {
|
||||
if (!isMouseOverWidget) {
|
||||
this._hideHelper();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._editor.onDidScrollChange(() => this._updatePositionIfVisible()));
|
||||
this._register(this._editor.onDidLayoutChange(() => this._updatePositionIfVisible()));
|
||||
}
|
||||
|
||||
// IOverlayWidget implementation
|
||||
public getId(): string {
|
||||
return SelectionHelperContribution.ID;
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._rootHTML;
|
||||
}
|
||||
|
||||
public getPosition(): IOverlayWidgetPosition | null {
|
||||
return null; // We position manually
|
||||
}
|
||||
|
||||
private _onSelectionChange(e: ICursorSelectionChangedEvent): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._editor.getModel().uri.scheme !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = this._editor.getSelection();
|
||||
|
||||
if (!selection || selection.isEmpty()) {
|
||||
this._hideHelper();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selection text to check if it's worth showing the helper
|
||||
const text = this._editor.getModel()!.getValueInRange(selection);
|
||||
if (text.length < 3) {
|
||||
this._hideHelper();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store selection
|
||||
this._lastSelection = new Selection(
|
||||
selection.startLineNumber,
|
||||
selection.startColumn,
|
||||
selection.endLineNumber,
|
||||
selection.endColumn
|
||||
);
|
||||
|
||||
this._showScheduler.schedule();
|
||||
}
|
||||
|
||||
// Update the _showHelperForSelection method to work with the React component
|
||||
private _showHelperForSelection(selection: Selection): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel()!;
|
||||
|
||||
// get the longest length of the nearest neighbors of the target
|
||||
const { tabSize: numSpacesInTab } = model.getFormattingOptions();
|
||||
const spaceWidth = this._editor.getOption(EditorOption.fontInfo).spaceWidth;
|
||||
const tabWidth = numSpacesInTab * spaceWidth;
|
||||
const numLinesModel = model.getLineCount()
|
||||
|
||||
// Calculate right edge of visible editor area
|
||||
const editorWidthPx = this._editor.getLayoutInfo().width;
|
||||
const maxLeftPx = editorWidthPx - minDistanceFromRightPx
|
||||
|
||||
// returns the position where the box should go on the targetLine
|
||||
const getBoxPosition = (targetLine: number): { top: number, left: number } => {
|
||||
|
||||
const targetPosition = this._editor.getScrolledVisiblePosition({ lineNumber: targetLine, column: 1 }) ?? { left: 0, top: 0 };
|
||||
|
||||
const { top: targetTop, left: targetLeft } = targetPosition
|
||||
|
||||
let targetWidth = 0;
|
||||
for (let i = targetLine; i <= targetLine + 1; i++) {
|
||||
|
||||
// if not in range, continue
|
||||
if (!(i >= 1) || !(i <= numLinesModel)) continue;
|
||||
|
||||
const content = model.getLineContent(i);
|
||||
const currWidth = getLengthOfTextPx({
|
||||
tabWidth,
|
||||
spaceWidth,
|
||||
content
|
||||
})
|
||||
|
||||
targetWidth = Math.max(targetWidth, currWidth);
|
||||
}
|
||||
|
||||
return {
|
||||
top: targetTop,
|
||||
left: targetLeft + targetWidth,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Calculate the middle line of the selection
|
||||
const startLine = selection.startLineNumber;
|
||||
const endLine = selection.endLineNumber;
|
||||
// const middleLine = Math.floor(startLine + (endLine - startLine) / 2);
|
||||
const targetLine = endLine - startLine + 1 <= 2 ? startLine : startLine + 2;
|
||||
|
||||
let boxPos = getBoxPosition(targetLine);
|
||||
|
||||
// if the position of the box is too far to the right, keep searching for a good position
|
||||
const lineDeltasToTry = [-1, -2, -3, 1, 2, 3];
|
||||
|
||||
if (boxPos.left > maxLeftPx) {
|
||||
for (const lineDelta of lineDeltasToTry) {
|
||||
|
||||
boxPos = getBoxPosition(targetLine + lineDelta);
|
||||
if (boxPos.left <= maxLeftPx) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (boxPos.left > maxLeftPx) { // if still not found, make it 2 lines before
|
||||
boxPos = getBoxPosition(targetLine - 2)
|
||||
}
|
||||
|
||||
|
||||
// Position the helper element at the end of the middle line but ensure it's visible
|
||||
const xPosition = Math.max(Math.min(boxPos.left, maxLeftPx), minLeftPx);
|
||||
const yPosition = boxPos.top;
|
||||
|
||||
// Update the React component position
|
||||
this._rootHTML.style.left = `${xPosition}px`;
|
||||
this._rootHTML.style.top = `${yPosition}px`;
|
||||
this._rootHTML.style.display = 'flex'; // Show the container
|
||||
|
||||
this._isVisible = true;
|
||||
|
||||
// rerender
|
||||
const enabled = this._voidSettingsService.state.globalSettings.showInlineSuggestions
|
||||
&& this._editor.hasTextFocus() // needed since VS Code counts unfocused selections as selections, which causes this to rerender when it shouldnt (bad ux)
|
||||
|
||||
if (enabled) {
|
||||
this._rerender({ rerenderKey: this._rerenderKey } satisfies VoidSelectionHelperProps)
|
||||
this._rerenderKey = (this._rerenderKey + 1) % 2;
|
||||
// this._reactComponentRerender();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private _hideHelper(): void {
|
||||
this._rootHTML.style.display = 'none';
|
||||
this._isVisible = false;
|
||||
this._lastSelection = null;
|
||||
}
|
||||
|
||||
private _updatePositionIfVisible(): void {
|
||||
if (!this._isVisible || !this._lastSelection || !this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showHelperForSelection(this._lastSelection);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._hideHelper();
|
||||
if (this._reactComponentDisposable) {
|
||||
this._reactComponentDisposable.dispose();
|
||||
}
|
||||
this._editor.removeOverlayWidget(this);
|
||||
this._showScheduler.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Register the contribution
|
||||
registerEditorContribution(SelectionHelperContribution.ID, SelectionHelperContribution, EditorContributionInstantiation.Eager);
|
||||
|
|
@ -8,57 +8,121 @@ 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,
|
||||
label: `Keep Void outdated`,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => {
|
||||
notifController.close()
|
||||
}
|
||||
}]
|
||||
},
|
||||
})
|
||||
const d = notifController.onDidClose(() => {
|
||||
notifyYesUpdate(notifService, res)
|
||||
d.dispose()
|
||||
})
|
||||
}
|
||||
const notifyNoUpdate = (notifService: INotificationService) => {
|
||||
notifService.notify({
|
||||
}
|
||||
}
|
||||
else {
|
||||
actions = undefined
|
||||
}
|
||||
|
||||
const notifController = notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: 'Void is up-to-date!',
|
||||
message: message,
|
||||
sticky: true,
|
||||
progress: actions ? { worked: 0, total: 100 } : undefined,
|
||||
actions: actions,
|
||||
})
|
||||
// const d = notifController.onDidClose(() => {
|
||||
// notifyYesUpdate(notifService, res)
|
||||
// d.dispose()
|
||||
// })
|
||||
}
|
||||
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)!`
|
||||
|
|
@ -70,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 {
|
||||
|
|
@ -84,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()
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -97,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()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,40 +5,31 @@
|
|||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
|
||||
import { AnthropicReasoning } from './sendLLMMessageTypes.js';
|
||||
import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
import { AnthropicReasoning, RawToolCallObj } from './sendLLMMessageTypes.js';
|
||||
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
paramsStr: string; // internal use
|
||||
id: string; // apis require this tool use id
|
||||
content: string; // give this result to LLM (string of value)
|
||||
} & (
|
||||
// in order of events:
|
||||
| { type: 'invalid_params', result: null, params: null, name: string }
|
||||
| { type: 'invalid_params', result: null, name: T, params: RawToolCallObj | null, }
|
||||
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
|
||||
|
||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
|
||||
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
|
||||
| { type: 'success', result: ToolResultType[T], name: T, params: ToolCallParams[T], }
|
||||
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T], }
|
||||
) // user rejected
|
||||
|
||||
export type DecorativeCanceledTool = {
|
||||
role: 'decorative_canceled_tool';
|
||||
name: string;
|
||||
role: 'interrupted_streaming_tool';
|
||||
name: ToolName;
|
||||
}
|
||||
|
||||
// export type ToolRequestApproval<T extends ToolName> = {
|
||||
// role: 'tool_request';
|
||||
// name: T; // internal use
|
||||
// params: ToolCallParams[T]; // internal use
|
||||
// paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
|
||||
// id: string; // proposed tool's id
|
||||
// }
|
||||
|
||||
|
||||
// checkpoints
|
||||
export type CheckpointEntry = {
|
||||
|
|
@ -65,8 +56,9 @@ export type ChatMessage =
|
|||
}
|
||||
} | {
|
||||
role: 'assistant';
|
||||
content: string; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnText } from '../sendLLMMessageTypes.js'
|
||||
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
|
||||
|
||||
class SurroundingsRemover {
|
||||
export class SurroundingsRemover {
|
||||
readonly originalS: string
|
||||
i: number
|
||||
j: number
|
||||
|
|
@ -60,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)
|
||||
|
||||
|
|
@ -174,7 +173,7 @@ export type ExtractedSearchReplaceBlock = {
|
|||
// JS substring swaps indices, so "ab".substr(1,0) will NOT be '', it will be 'a'!
|
||||
const voidSubstr = (str: string, start: number, end: number) => end < start ? '' : str.substring(start, end)
|
||||
|
||||
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
|
||||
export const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
|
||||
// for each prefix
|
||||
for (let i = anyPrefix.length; i >= 1; i--) { // i >= 1 because must not be empty string
|
||||
const prefix = anyPrefix.slice(0, i)
|
||||
|
|
@ -250,122 +249,6 @@ export const extractSearchReplaceBlocks = (str: string) => {
|
|||
|
||||
|
||||
|
||||
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
|
||||
export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => {
|
||||
let latestAddIdx = 0 // exclusive index in fullText_
|
||||
let foundTag1 = false
|
||||
let foundTag2 = false
|
||||
|
||||
let fullTextSoFar = ''
|
||||
let fullReasoningSoFar = ''
|
||||
|
||||
let onText_ = onText
|
||||
onText = (params) => {
|
||||
onText_(params)
|
||||
}
|
||||
|
||||
const newOnText: OnText = ({ fullText: fullText_, ...p }) => {
|
||||
// until found the first think tag, keep adding to fullText
|
||||
if (!foundTag1) {
|
||||
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
|
||||
if (endsWithTag1) {
|
||||
// console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ })
|
||||
// wait until we get the full tag or know more
|
||||
return
|
||||
}
|
||||
// if found the first tag
|
||||
const tag1Index = fullText_.indexOf(thinkTags[0])
|
||||
if (tag1Index !== -1) {
|
||||
// console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ })
|
||||
foundTag1 = true
|
||||
// Add text before the tag to fullTextSoFar
|
||||
fullTextSoFar += fullText_.substring(0, tag1Index)
|
||||
// Update latestAddIdx to after the first tag
|
||||
latestAddIdx = tag1Index + thinkTags[0].length
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar })
|
||||
// add the text to fullText
|
||||
fullTextSoFar = fullText_
|
||||
latestAddIdx = fullText_.length
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// at this point, we found <tag1>
|
||||
|
||||
// until found the second think tag, keep adding to fullReasoning
|
||||
if (!foundTag2) {
|
||||
const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1])
|
||||
if (endsWithTag2) {
|
||||
// console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar })
|
||||
// wait until we get the full tag or know more
|
||||
return
|
||||
}
|
||||
|
||||
// if found the second tag
|
||||
const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx)
|
||||
if (tag2Index !== -1) {
|
||||
// console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar })
|
||||
foundTag2 = true
|
||||
// Add everything between first and second tag to reasoning
|
||||
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
|
||||
// Update latestAddIdx to after the second tag
|
||||
latestAddIdx = tag2Index + thinkTags[1].length
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// add the text to fullReasoning (content after first tag but before second tag)
|
||||
// console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar })
|
||||
|
||||
// If we have more text than we've processed, add it to reasoning
|
||||
if (fullText_.length > latestAddIdx) {
|
||||
fullReasoningSoFar += fullText_.substring(latestAddIdx)
|
||||
latestAddIdx = fullText_.length
|
||||
}
|
||||
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// at this point, we found <tag2> - content after the second tag is normal text
|
||||
// console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar })
|
||||
|
||||
// Add any new text after the closing tag to fullTextSoFar
|
||||
if (fullText_.length > latestAddIdx) {
|
||||
fullTextSoFar += fullText_.substring(latestAddIdx)
|
||||
latestAddIdx = fullText_.length
|
||||
}
|
||||
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
}
|
||||
|
||||
return newOnText
|
||||
}
|
||||
|
||||
|
||||
export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => {
|
||||
const tag1Idx = fullText_.indexOf(thinkTags[0])
|
||||
const tag2Idx = fullText_.indexOf(thinkTags[1])
|
||||
if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning
|
||||
if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning
|
||||
|
||||
const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx)
|
||||
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
|
||||
return { fullText, fullReasoning }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,46 @@
|
|||
import { FeatureName, ModelSelectionOptions, ProviderName } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const defaultProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAI: {
|
||||
apiKey: '',
|
||||
},
|
||||
deepseek: {
|
||||
apiKey: '',
|
||||
},
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
vLLM: {
|
||||
endpoint: 'http://localhost:8000',
|
||||
},
|
||||
openRouter: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAICompatible: {
|
||||
endpoint: '',
|
||||
apiKey: '',
|
||||
},
|
||||
gemini: {
|
||||
apiKey: '',
|
||||
},
|
||||
groq: {
|
||||
apiKey: '',
|
||||
},
|
||||
xAI: {
|
||||
apiKey: ''
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
|
||||
|
||||
export const defaultModelsOfProvider = {
|
||||
openAI: [ // https://platform.openai.com/docs/models/gp
|
||||
'o3-mini',
|
||||
|
|
@ -38,15 +78,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',
|
||||
],
|
||||
|
|
@ -67,20 +109,21 @@ export const defaultModelsOfProvider = {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type ModelOptions = {
|
||||
export type VoidStaticModelInfo = { // not stateful
|
||||
contextWindow: number; // input tokens
|
||||
maxOutputTokens: number | null; // output tokens, defaults to 4092
|
||||
cost: { // <-- UNUSED
|
||||
cost: { // <-- UNUSED
|
||||
input: number;
|
||||
output: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
}
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated';
|
||||
supportsTools: false | 'anthropic-style' | 'openai-style';
|
||||
|
||||
downloadable: false | {
|
||||
sizeGb: number | 'not-known'
|
||||
}
|
||||
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special paramete
|
||||
supportsFIM: boolean;
|
||||
|
||||
reasoningCapabilities: false | {
|
||||
|
|
@ -108,20 +151,20 @@ type ProviderReasoningIOSettings = {
|
|||
| { nameOfFieldInDelta?: undefined, needsManualParse?: true, };
|
||||
}
|
||||
|
||||
type ProviderSettings = {
|
||||
type VoidStaticProviderInfo = { // doesn't change (not stateful)
|
||||
providerReasoningIOSettings?: ProviderReasoningIOSettings; // input/output settings around thinking (allowed to be empty) - only applied if the model supports reasoning output
|
||||
modelOptions: { [key: string]: ModelOptions };
|
||||
modelOptionsFallback: (modelName: string) => (ModelOptions & { modelName: string }) | null;
|
||||
modelOptions: { [key: string]: VoidStaticModelInfo };
|
||||
modelOptionsFallback: (modelName: string, fallbackKnownValues?: Partial<VoidStaticModelInfo>) => (VoidStaticModelInfo & { modelName: string }) | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const modelOptionsDefaults: ModelOptions = {
|
||||
const modelOptionsDefaults: VoidStaticModelInfo = {
|
||||
contextWindow: 32_000,
|
||||
maxOutputTokens: 4_096,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsFIM: false,
|
||||
reasoningCapabilities: false,
|
||||
}
|
||||
|
|
@ -130,42 +173,36 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
'deepseekR1': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'deepseekCoderV3': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: false, // unstable
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'deepseekCoderV2': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: false, // unstable
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'codestral': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'openhands-lm-32b': { // https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false, // built on qwen 2.5 32B instruct
|
||||
contextWindow: 128_000, maxOutputTokens: 4_096
|
||||
},
|
||||
'phi4': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 16_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
|
|
@ -173,7 +210,6 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
'gemma': { // https://news.ycombinator.com/item?id=43451406
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
|
|
@ -181,14 +217,12 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
'llama4-scout': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 10_000_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'llama4-maverick': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 10_000_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
|
|
@ -197,28 +231,24 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
'llama3': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'llama3.1': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'llama3.2': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'llama3.3': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
|
|
@ -226,14 +256,12 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
'qwen2.5coder': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'qwq': {
|
||||
supportsFIM: false, // no FIM, yes reasoning
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
contextWindow: 128_000, maxOutputTokens: 8_192,
|
||||
},
|
||||
|
|
@ -241,7 +269,6 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
'starcoder2': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 128_000, maxOutputTokens: 8_192,
|
||||
|
||||
|
|
@ -249,31 +276,36 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
'codegemma:2b': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 128_000, maxOutputTokens: 8_192,
|
||||
|
||||
},
|
||||
} as const satisfies { [s: string]: Omit<ModelOptions, 'cost'> }
|
||||
'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> }
|
||||
|
||||
|
||||
|
||||
|
||||
const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => {
|
||||
const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (modelName, fallbackKnownValues) => {
|
||||
|
||||
|
||||
const lower = modelName.toLowerCase()
|
||||
|
||||
const toFallback = (opts: Omit<ModelOptions, 'cost'>): ModelOptions & { modelName: string } => {
|
||||
const toFallback = (opts: Omit<VoidStaticModelInfo, 'cost' | 'downloadable'>): VoidStaticModelInfo & { modelName: string } => {
|
||||
return {
|
||||
modelName,
|
||||
...opts,
|
||||
supportsSystemMessage: opts.supportsSystemMessage ? 'system-role' : false,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
...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'])
|
||||
|
|
@ -304,6 +336,8 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN
|
|||
|
||||
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('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'])
|
||||
|
|
@ -311,6 +345,9 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
@ -325,9 +362,9 @@ const anthropicModelOptions = {
|
|||
contextWindow: 200_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
reasoningCapabilities: {
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: true,
|
||||
|
|
@ -340,40 +377,40 @@ const anthropicModelOptions = {
|
|||
contextWindow: 200_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'claude-3-5-haiku-20241022': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'claude-3-opus-20240229': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: 4_096,
|
||||
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'claude-3-sonnet-20240229': { // no point of using this, but including this for people who put it in
|
||||
contextWindow: 200_000, cost: { input: 3.00, output: 15.00 },
|
||||
downloadable: false,
|
||||
maxOutputTokens: 4_096,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
reasoningCapabilities: false,
|
||||
}
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const anthropicSettings: ProviderSettings = {
|
||||
const anthropicSettings: VoidStaticProviderInfo = {
|
||||
providerReasoningIOSettings: {
|
||||
input: {
|
||||
includeInPayload: (reasoningInfo) => {
|
||||
|
|
@ -405,8 +442,8 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
contextWindow: 128_000,
|
||||
maxOutputTokens: 100_000,
|
||||
cost: { input: 15.00, cache_read: 7.50, output: 60.00, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: false,
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, // it doesn't actually output reasoning, but our logic is fine with it
|
||||
},
|
||||
|
|
@ -414,8 +451,8 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
contextWindow: 200_000,
|
||||
maxOutputTokens: 100_000,
|
||||
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: false,
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false },
|
||||
},
|
||||
|
|
@ -423,8 +460,8 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
contextWindow: 128_000,
|
||||
maxOutputTokens: 16_384,
|
||||
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -432,8 +469,8 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
contextWindow: 128_000,
|
||||
maxOutputTokens: 65_536,
|
||||
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: false,
|
||||
supportsSystemMessage: false, // does not support any system
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false },
|
||||
},
|
||||
|
|
@ -441,15 +478,15 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
contextWindow: 128_000,
|
||||
maxOutputTokens: 16_384,
|
||||
cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role', // ??
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
|
||||
const openAISettings: ProviderSettings = {
|
||||
const openAISettings: VoidStaticProviderInfo = {
|
||||
modelOptions: openAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const lower = modelName.toLowerCase()
|
||||
|
|
@ -468,14 +505,14 @@ const xAIModelOptions = {
|
|||
contextWindow: 131_072,
|
||||
maxOutputTokens: null, // 131_072,
|
||||
cost: { input: 2.00, output: 10.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const xAISettings: ProviderSettings = {
|
||||
const xAISettings: VoidStaticProviderInfo = {
|
||||
modelOptions: xAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const lower = modelName.toLowerCase()
|
||||
|
|
@ -493,59 +530,59 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
|||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gemini-2.0-flash': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: 8_192, // 8_192,
|
||||
cost: { input: 0.10, output: 0.40 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gemini-2.0-flash-lite-preview-02-05': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: 8_192, // 8_192,
|
||||
cost: { input: 0.075, output: 0.30 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gemini-1.5-flash': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: 8_192, // 8_192,
|
||||
cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gemini-1.5-pro': {
|
||||
contextWindow: 2_097_152,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gemini-1.5-flash-8b': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const geminiSettings: ProviderSettings = {
|
||||
const geminiSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: geminiModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
|
@ -559,17 +596,19 @@ const deepseekModelOptions = {
|
|||
contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing
|
||||
maxOutputTokens: 8_000, // 8_000,
|
||||
cost: { cache_read: .07, input: .27, output: 1.10, },
|
||||
downloadable: false,
|
||||
},
|
||||
'deepseek-reasoner': {
|
||||
...openSourceModelOptions_assumingOAICompat.deepseekCoderV2,
|
||||
contextWindow: 64_000,
|
||||
maxOutputTokens: 8_000, // 8_000,
|
||||
cost: { cache_read: .14, input: .55, output: 2.19, },
|
||||
downloadable: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
|
||||
const deepseekSettings: ProviderSettings = {
|
||||
const deepseekSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: deepseekModelOptions,
|
||||
providerReasoningIOSettings: {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model
|
||||
|
|
@ -642,40 +681,40 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq
|
|||
contextWindow: 128_000,
|
||||
maxOutputTokens: 32_768, // 32_768,
|
||||
cost: { input: 0.59, output: 0.79 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'llama-3.1-8b-instant': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0.05, output: 0.08 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwen-2.5-coder-32b': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null, // not specified?
|
||||
cost: { input: 0.79, output: 0.79 },
|
||||
downloadable: false,
|
||||
supportsFIM: false, // unfortunately looks like no FIM support on groq
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwen-qwq-32b': { // https://huggingface.co/Qwen/QwQ-32B
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null, // not specified?
|
||||
cost: { input: 0.29, output: 0.39 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] }, // we're using reasoning_format:parsed so really don't need to know openSourceThinkTags
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
const groqSettings: ProviderSettings = {
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
const groqSettings: VoidStaticProviderInfo = {
|
||||
providerReasoningIOSettings: {
|
||||
input: {
|
||||
includeInPayload: (reasoningInfo) => {
|
||||
|
|
@ -691,23 +730,67 @@ const groqSettings: ProviderSettings = {
|
|||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
||||
const ollamaModelOptions = {
|
||||
'qwen2.5-coder:3b': {
|
||||
contextWindow: 32_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: { sizeGb: 1.9 },
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwen2.5-coder': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: { sizeGb: 4.7 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwq': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: 32_000,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: { sizeGb: 20 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
'deepseek-r1': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: { sizeGb: 4.7 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
|
||||
export const ollamaRecommendedModels = ['qwen2.5-coder:3b', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
|
||||
|
||||
|
||||
|
||||
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
|
||||
const vLLMSettings: ProviderSettings = {
|
||||
|
||||
const vLLMSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
|
||||
modelOptions: {},
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
const ollamaSettings: ProviderSettings = {
|
||||
const ollamaSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: we need to filter out reasoning <think> tags manually
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
|
||||
modelOptions: {},
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: ollamaModelOptions,
|
||||
}
|
||||
|
||||
const openaiCompatible: ProviderSettings = {
|
||||
const openaiCompatible: VoidStaticProviderInfo = {
|
||||
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
|
||||
modelOptions: {},
|
||||
|
|
@ -720,8 +803,8 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
|||
contextWindow: 128_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -729,8 +812,8 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
|||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -738,8 +821,8 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
|||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -747,8 +830,8 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
|||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -757,14 +840,15 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
|||
contextWindow: 128_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0.8, output: 2.4 },
|
||||
downloadable: false,
|
||||
},
|
||||
'anthropic/claude-3.7-sonnet:thinking': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 3.00, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: { // same as anthropic, see above
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: false,
|
||||
|
|
@ -777,18 +861,18 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
|||
contextWindow: 200_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 3.00, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false, // stupidly, openrouter separates thinking from non-thinking
|
||||
},
|
||||
'anthropic/claude-3.5-sonnet': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 3.00, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'mistralai/codestral-2501': {
|
||||
|
|
@ -796,26 +880,26 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
|||
contextWindow: 256_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0.3, output: 0.9 },
|
||||
supportsTools: 'openai-style',
|
||||
downloadable: false,
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwen/qwen-2.5-coder-32b-instruct': {
|
||||
...openSourceModelOptions_assumingOAICompat['qwen2.5coder'],
|
||||
contextWindow: 33_000,
|
||||
maxOutputTokens: null,
|
||||
supportsTools: false, // openrouter qwen doesn't seem to support tools...?
|
||||
cost: { input: 0.07, output: 0.16 },
|
||||
downloadable: false,
|
||||
},
|
||||
'qwen/qwq-32b': {
|
||||
...openSourceModelOptions_assumingOAICompat['qwq'],
|
||||
contextWindow: 33_000,
|
||||
maxOutputTokens: null,
|
||||
supportsTools: false, // openrouter qwen doesn't seem to support tools...?
|
||||
cost: { input: 0.07, output: 0.16 },
|
||||
downloadable: false,
|
||||
}
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const openRouterSettings: ProviderSettings = {
|
||||
const openRouterSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
|
||||
providerReasoningIOSettings: {
|
||||
input: {
|
||||
|
|
@ -842,7 +926,7 @@ const openRouterSettings: ProviderSettings = {
|
|||
|
||||
// ---------------- model settings of everything above ----------------
|
||||
|
||||
const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSettings } = {
|
||||
const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProviderInfo } = {
|
||||
openAI: openAISettings,
|
||||
anthropic: anthropicSettings,
|
||||
xAI: xAISettings,
|
||||
|
|
@ -867,8 +951,10 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting
|
|||
// ---------------- exports ----------------
|
||||
|
||||
// returns the capabilities and the adjusted modelName if it was a fallback
|
||||
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => {
|
||||
export const getModelCapabilities = (providerName: ProviderName, modelName: string): VoidStaticModelInfo & { modelName: string; isUnrecognizedModel: boolean } => {
|
||||
|
||||
const lowercaseModelName = modelName.toLowerCase()
|
||||
|
||||
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
|
||||
|
||||
// search model options object directly first
|
||||
|
|
|
|||
|
|
@ -3,16 +3,29 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
import { InternalToolInfo } from '../toolsServiceTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolCallObj } from '../sendLLMMessageTypes.js';
|
||||
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
|
||||
import { 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
|
||||
|
||||
|
||||
// ======================================================== tools ========================================================
|
||||
const changesExampleContent = `\
|
||||
// ... existing code ...
|
||||
// {{change 1}}
|
||||
|
|
@ -22,35 +35,34 @@ const changesExampleContent = `\
|
|||
// {{change 3}}
|
||||
// ... existing code ...`
|
||||
|
||||
const editToolDescription = `\
|
||||
const editToolDescriptionExample = `\
|
||||
${tripleTick[0]}
|
||||
${changesExampleContent}
|
||||
${tripleTick[1]}`
|
||||
|
||||
const fileNameEdit = `${tripleTick[0]}typescript
|
||||
const fileNameEditExample = `${tripleTick[0]}typescript
|
||||
/Users/username/Dekstop/my_project/app.ts
|
||||
${changesExampleContent}
|
||||
${tripleTick[1]}`
|
||||
|
||||
|
||||
|
||||
export type InternalToolInfo = {
|
||||
name: string,
|
||||
description: string,
|
||||
params: {
|
||||
[paramName: string]: { description: string }
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// ======================================================== tools ========================================================
|
||||
|
||||
const paginationHelper = {
|
||||
desc: `Very large results may be paginated (a note will always be included if pagination took place). Pagination fails gracefully if out of bounds or invalid page number.`,
|
||||
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
|
||||
} as const
|
||||
|
||||
const uriParam = (object: string) => ({
|
||||
uri: { type: 'string', description: `The FULL path to the ${object}.` }
|
||||
uri: { description: `The FULL path to the ${object}.` }
|
||||
})
|
||||
|
||||
|
||||
const searchParams = {
|
||||
searchInFolder: { type: 'string', description: 'Only search files in this given folder. Leave as empty to search all available files.' },
|
||||
isRegex: { type: 'string', description: 'Whether to treat the query as a regular expression. Default is "false".' },
|
||||
const paginationParam = {
|
||||
page_number: { description: 'Optional. The page number of the result. Default is 1.' }
|
||||
} as const
|
||||
|
||||
|
||||
|
|
@ -59,49 +71,64 @@ export const voidTools = {
|
|||
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
|
||||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
startLine: { type: 'string', description: 'Line to start reading from. Default is "null", treated as 1.' },
|
||||
endLine: { type: 'string', description: 'Line to stop reading from (inclusive). Default is "null", treated as Infinity.' },
|
||||
...paginationHelper.param,
|
||||
start_line: { description: 'Optional. Default is 1. Start reading on this line.' },
|
||||
end_line: { description: 'Optional. Default is Infinity. Stop reading after this line.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
ls_dir: {
|
||||
name: 'ls_dir',
|
||||
description: `Returns all file names and folder names in a given folder. ${paginationHelper.desc}`,
|
||||
description: `Lists all files and folders in the given URI.`,
|
||||
params: {
|
||||
...uriParam('folder'),
|
||||
...paginationHelper.param,
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
get_dir_structure: {
|
||||
name: 'get_dir_structure',
|
||||
description: `This is a very effective way to learn about the user's codebase. You might want to use this instead of ls_dir. Returns a tree diagram of all the files and folders in the given folder URI. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`,
|
||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
}
|
||||
},
|
||||
|
||||
// pathname_search: {
|
||||
// name: 'pathname_search',
|
||||
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
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. ${paginationHelper.desc}`,
|
||||
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: { type: 'string', description: undefined },
|
||||
...searchParams,
|
||||
...paginationHelper.param,
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
search_files: {
|
||||
name: 'search_files',
|
||||
description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`,
|
||||
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.`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...searchParams,
|
||||
...paginationHelper.param,
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Returns all lint errors on a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -109,7 +136,7 @@ export const voidTools = {
|
|||
|
||||
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. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
|
||||
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'),
|
||||
},
|
||||
|
|
@ -117,25 +144,25 @@ export const voidTools = {
|
|||
|
||||
delete_file_or_folder: {
|
||||
name: 'delete_file_or_folder',
|
||||
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
|
||||
description: `Delete a file or folder at the given path.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
params: { type: 'string', description: 'Return -r here to delete recursively (if applicable). Default is the empty string.' }
|
||||
params: { description: 'Optional. Return -r here to delete recursively.' }
|
||||
},
|
||||
},
|
||||
|
||||
edit_file: { // APPLY TOOL
|
||||
name: 'edit_file',
|
||||
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
|
||||
description: `Edits the contents of a file given the file's URI and a description.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
changeDescription: {
|
||||
type: 'string', description: `\
|
||||
- Your changeDescription should be 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, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
|
||||
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
|
||||
- You must output your description in triple backticks.
|
||||
Here's an example of a good description:\n${editToolDescription}.`
|
||||
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. \
|
||||
Here's an example of a good description:\n${editToolDescriptionExample}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -144,161 +171,199 @@ Here's an example of a good description:\n${editToolDescription}.`
|
|||
name: 'run_terminal_command',
|
||||
description: `Executes a terminal command.`,
|
||||
params: {
|
||||
command: { type: 'string', description: 'The terminal command to execute. Typically you should pipe to cat to avoid pagination.' },
|
||||
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
|
||||
terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
|
||||
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.' },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
} satisfies { [name: string]: InternalToolInfo }
|
||||
|
||||
|
||||
export type ToolName = keyof typeof voidTools
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
|
||||
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
|
||||
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
export const availableTools = (chatMode: ChatMode) => {
|
||||
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))
|
||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
||||
: undefined
|
||||
|
||||
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
|
||||
return tools
|
||||
}
|
||||
|
||||
const availableXMLToolsStr = (tools: InternalToolInfo[]) => {
|
||||
return `${tools.map((t, i) => {
|
||||
const params = Object.keys(t.params).map(paramName => `<${paramName}>${t.params[paramName].description}</${paramName}>`).join('\n')
|
||||
return `\
|
||||
${i + 1}. ${t.name}
|
||||
Description: ${t.description}
|
||||
Format:
|
||||
<${t.name}>${!params ? '' : `\n${params}`}
|
||||
</${t.name}>`
|
||||
}).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')
|
||||
return `\
|
||||
<${toolCall.name}>${!params ? '' : `\n${params}`}
|
||||
</${toolCall.name}>`
|
||||
.replace('\t', ' ')
|
||||
}
|
||||
|
||||
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
|
||||
// - 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 ''
|
||||
|
||||
const toolXMLDefinitions = (`\
|
||||
Available tools:
|
||||
|
||||
${availableXMLToolsStr(tools)}`)
|
||||
|
||||
const toolCallXMLGuidelines = (`\
|
||||
Tool calling details:
|
||||
- Once you write a tool call, you must STOP and WAIT for the result.
|
||||
- All parameters are REQUIRED unless noted otherwise.
|
||||
- To call a tool, write its name and parameters in one of the XML formats specified above.
|
||||
- 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.`)
|
||||
|
||||
return `\
|
||||
${toolXMLDefinitions}
|
||||
|
||||
${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 }) => {
|
||||
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.`
|
||||
: mode === 'normal' ? `to assist the user with their coding tasks.`
|
||||
: ''}
|
||||
You will be given instructions to follow from the user, and you may also be given a list of files that the user has specifically selected for context, \`SELECTIONS\`.
|
||||
Please assist the user with their query.`)
|
||||
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\
|
||||
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the user's IDE called Void. Your job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.`
|
||||
: mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.`
|
||||
: mode === 'normal' ? `to assist the user with their coding tasks.`
|
||||
: ''}
|
||||
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
|
||||
Please assist the user with their query. The user's query is never invalid.
|
||||
${/* system info */''}
|
||||
The user's system information is as follows:
|
||||
|
||||
|
||||
const sysInfo = (`Here is the user's system information:
|
||||
<system_info>
|
||||
- ${os}
|
||||
- Open workspace(s): ${workspaceFolders.join(', ') || 'NO WORKSPACE OPEN'}
|
||||
- Open tab(s): ${openedURIs.join(', ') || 'NO OPENED EDITORS'}
|
||||
- Active tab: ${activeURI}
|
||||
${(mode === 'agent') && runningTerminalIds.length !== 0 ? `
|
||||
|
||||
- Open workspaces:
|
||||
${workspaceFolders.join('\n') || 'NO WORKSPACE OPEN'}
|
||||
|
||||
- Active file:
|
||||
${activeURI}
|
||||
|
||||
- Open files:
|
||||
${openedURIs.join('\n') || 'NO OPENED EDITORS'}${''/* separator */}${mode === 'agent' && runningTerminalIds.length !== 0 ? `
|
||||
|
||||
- Existing terminal IDs: ${runningTerminalIds.join(', ')}` : ''}
|
||||
|
||||
${/* tool use */ mode === 'agent' || mode === 'gather' ? `\
|
||||
You will be given tools you can call.
|
||||
${mode === 'agent' ? `\
|
||||
- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.
|
||||
- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.
|
||||
- You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.
|
||||
- ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`
|
||||
: mode === 'gather' ? `\
|
||||
- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.
|
||||
- You should extensively read files, types, etc and gather relevant context.`
|
||||
: ''}
|
||||
- If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc.
|
||||
- NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results.
|
||||
- Some tools only work if the user has a workspace open.${mode === 'agent' ? `
|
||||
- NEVER modify a file outside the user's workspace(s) without permission from the user.` : ''}
|
||||
\
|
||||
`: `\
|
||||
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
|
||||
\
|
||||
`}
|
||||
${/* code blocks */ mode === 'agent' ? `\
|
||||
Behavior:
|
||||
- Always use tools (edit, terminal, etc) to take actions and implement changes. Don't just describe them.
|
||||
- Prioritize taking as many steps as you need to complete your request over stopping early.\
|
||||
`: `\
|
||||
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
|
||||
- The first line of the code block must be the FULL PATH of the file you want to change.
|
||||
- The remaining contents should be 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, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
|
||||
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
|
||||
Here's an example of a good code block:\n${fileNameEdit}.
|
||||
|
||||
If you write a code block that's related to a specific file, please use the same format as above:
|
||||
- The first line of the code block must be the FULL PATH of the related file if known.
|
||||
- The remaining contents of the file should proceed as usual.
|
||||
\
|
||||
`}
|
||||
${/* misc */''}
|
||||
Misc:
|
||||
- Do not make things up.
|
||||
- Do not be lazy.
|
||||
- NEVER re-write the entire file.
|
||||
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.
|
||||
- Today's date is ${new Date().toDateString()}
|
||||
The user's codebase is structured as follows:\n${directoryStr}
|
||||
\
|
||||
`
|
||||
// agent mode doesn't know about 1st line paths yet
|
||||
// - If you wrote triple ticks and ___, then include the file's full path in the first line of the triple ticks. This is only for display purposes to the user, and it's preferred but optional. Never do this in a tool parameter, or if there's ambiguity about the full path.
|
||||
</system_info>`)
|
||||
|
||||
|
||||
// type FileSelnLocal = { fileURI: URI, language: string, content: string }
|
||||
// const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => {
|
||||
// return `\
|
||||
// ${fileURI.fsPath}
|
||||
// ${tripleTick[0]}${language}
|
||||
// ${content}
|
||||
// ${tripleTick[1]}
|
||||
// `
|
||||
// }
|
||||
// const stringifyCodeSelection = ({ uri, language, range }: StagingSelectionItem & { type: 'CodeSelection' }) => {
|
||||
// return `\
|
||||
const fsInfo = (`Here is an overview of the user's file system:
|
||||
<files_overview>
|
||||
${directoryStr}
|
||||
</files_overview>`)
|
||||
|
||||
// ${tripleTick[0]}${language}
|
||||
// ${selectionStr}
|
||||
// ${tripleTick[1]}
|
||||
// `
|
||||
|
||||
const toolDefinitions = systemToolsXMLPrompt(mode)
|
||||
|
||||
const details: string[] = []
|
||||
|
||||
if (mode === 'agent' || mode === 'gather') {
|
||||
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
|
||||
details.push('Only use ONE tool call at a time, and always wait for the result before proceeding.') // XML
|
||||
details.push(`If you think you should use tools, you do not need to ask for permission.`)
|
||||
details.push(`NEVER say something like "I'm going to use \`tool_name\`". Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc.`)
|
||||
details.push(`Many tools only work if the user has a workspace open.`)
|
||||
}
|
||||
else {
|
||||
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
|
||||
}
|
||||
|
||||
if (mode === 'agent') {
|
||||
details.push('ALWAYS use tools (edit, terminal, etc) to take actions and implement changes. For example, if you would like to edit a file, you MUST use a tool.')
|
||||
details.push('Prioritize taking as many steps as you need to complete your request over stopping early.')
|
||||
details.push(`You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.`)
|
||||
details.push(`ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`)
|
||||
details.push(`NEVER modify a file outside the user's workspace(s) without permission from the user.`)
|
||||
}
|
||||
|
||||
if (mode === 'gather') {
|
||||
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.`)
|
||||
}
|
||||
|
||||
|
||||
if (mode === 'gather' || mode === 'normal') {
|
||||
details.push(`If you write any code blocks, please use this format:
|
||||
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
|
||||
- The remaining contents of the file should proceed as usual.`)
|
||||
|
||||
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
|
||||
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
|
||||
- The remaining contents should be \
|
||||
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. \
|
||||
Here's an example of a good edit suggestion:
|
||||
${fileNameEditExample}.`)
|
||||
}
|
||||
|
||||
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
|
||||
details.push(`Today's date is ${new Date().toDateString()}.`)
|
||||
|
||||
const importantDetails = (`Important notes:
|
||||
${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
|
||||
|
||||
|
||||
// return answer
|
||||
const ansStrs: string[] = []
|
||||
ansStrs.push(header)
|
||||
ansStrs.push(sysInfo)
|
||||
ansStrs.push(fsInfo)
|
||||
if (toolDefinitions) ansStrs.push(toolDefinitions)
|
||||
ansStrs.push(importantDetails)
|
||||
ansStrs.push('Now, please assist the user with their query.')
|
||||
|
||||
const fullSystemMsgStr = ansStrs
|
||||
.join('\n\n\n')
|
||||
.trim()
|
||||
.replace('\t', ' ')
|
||||
|
||||
return fullSystemMsgStr
|
||||
|
||||
}
|
||||
|
||||
|
||||
// // log all prompts
|
||||
// for (const chatMode of ['agent', 'gather', 'normal'] satisfies ChatMode[]) {
|
||||
// console.log(`========================================= SYSTEM MESSAGE FOR ${chatMode} ===================================\n`,
|
||||
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', runningTerminalIds: [], directoryStr: 'lol', }))
|
||||
// }
|
||||
|
||||
// const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
|
||||
// const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => {
|
||||
// if (fileSelections.length === 0) return null
|
||||
// const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
|
||||
// const { model } = await voidModelService.getModelSafe(sel.fileURI)
|
||||
// const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr
|
||||
// return { ...sel, content }
|
||||
// }))
|
||||
// return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// export const chat_selectionsString = async (
|
||||
// prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null,
|
||||
// voidModelService: IVoidModelService,
|
||||
// ) => {
|
||||
|
||||
// // ADD IN FILES AT TOP
|
||||
// const allSelections = [...currSelns || [], ...prevSelns || []]
|
||||
|
||||
// if (allSelections.length === 0) return null
|
||||
|
||||
// for (const selection of allSelections) {
|
||||
// if (selection.type === 'Selection') {
|
||||
// codeSelections.push(selection)
|
||||
// }
|
||||
// else if (selection.type === 'File') {
|
||||
// const fileSelection = selection
|
||||
// const path = fileSelection.fileURI.fsPath
|
||||
// if (!filesURIs.has(path)) {
|
||||
// filesURIs.add(path)
|
||||
// fileSelections.push(fileSelection)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const filesStr = await stringifyFileSelections(fileSelections, voidModelService)
|
||||
// const selnsStr = stringifyCodeSelections(codeSelections)
|
||||
|
||||
// const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n')
|
||||
// return fileContents || null
|
||||
// }
|
||||
|
||||
// export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => {
|
||||
// if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
|
||||
// else return userMessage
|
||||
// }
|
||||
|
||||
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
|
||||
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
|
||||
|
|
@ -453,8 +518,6 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
|
|||
|
||||
const fullFileLines = fullFileStr.split('\n')
|
||||
|
||||
// we can optimize this later
|
||||
const MAX_PREFIX_SUFFIX_CHARS = 20_000
|
||||
/*
|
||||
|
||||
a
|
||||
|
|
@ -527,10 +590,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
|
||||
|
|
@ -560,6 +629,40 @@ ${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 ========================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolName, InternalToolInfo } from './toolsServiceTypes.js'
|
||||
import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
import { ToolName, ToolParamName } from './prompt/prompts.js'
|
||||
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export const errorDetails = (fullError: Error | null): string | null => {
|
||||
|
|
@ -37,25 +37,24 @@ export type LLMChatMessage = {
|
|||
role: 'assistant',
|
||||
content: string; // text content
|
||||
anthropicReasoning: AnthropicReasoning[] | null;
|
||||
} | {
|
||||
role: 'tool';
|
||||
content: string; // result
|
||||
name: string;
|
||||
params: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
export type ToolCallType = {
|
||||
export type RawToolParamsObj = {
|
||||
[paramName in ToolParamName]?: string;
|
||||
}
|
||||
export type RawToolCallObj = {
|
||||
name: ToolName;
|
||||
paramsStr: string;
|
||||
id: string;
|
||||
}
|
||||
rawParams: RawToolParamsObj;
|
||||
doneParams: ToolParamName[];
|
||||
isDone: boolean;
|
||||
};
|
||||
|
||||
|
||||
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
|
||||
|
||||
export type OnText = (p: { fullText: string; fullReasoning: string; fullToolName: string; fullToolParams: string; }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
|
||||
export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
|
||||
export type OnError = (p: { message: string; fullError: Error | null }) => void
|
||||
export type OnAbort = () => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
|
@ -70,11 +69,11 @@ export type LLMFIMMessage = {
|
|||
type SendLLMType = {
|
||||
messagesType: 'chatMessages';
|
||||
messages: LLMChatMessage[];
|
||||
tools?: InternalToolInfo[];
|
||||
chatMode: ChatMode | null;
|
||||
} | {
|
||||
messagesType: 'FIMMessage';
|
||||
messages: LLMFIMMessage;
|
||||
tools?: undefined;
|
||||
chatMode?: undefined;
|
||||
}
|
||||
|
||||
// service types
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { voidTools } from './prompt/prompts.js';
|
||||
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
|
||||
|
||||
|
||||
export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
|
||||
|
||||
export type LintErrorItem = { code: string, message: string, startLineNumber: number, endLineNumber: number }
|
||||
|
||||
// Partial of IFileStat
|
||||
export type ShallowDirectoryItem = {
|
||||
uri: URI;
|
||||
|
|
@ -14,26 +15,6 @@ export type ShallowDirectoryItem = {
|
|||
isSymbolicLink: boolean;
|
||||
}
|
||||
|
||||
// we do this using Anthropic's style and convert to OpenAI style later
|
||||
export type InternalToolInfo = {
|
||||
name: string,
|
||||
description: string,
|
||||
params: {
|
||||
[paramName: string]: { type: string, description: string | undefined } // name -> type
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export type ToolName = keyof typeof voidTools
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
|
||||
const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'run_terminal_command'] as const satisfies readonly ToolName[]
|
||||
|
|
@ -45,8 +26,9 @@ export type ToolCallParams = {
|
|||
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
||||
'ls_dir': { rootURI: URI, pageNumber: number },
|
||||
'get_dir_structure': { rootURI: URI },
|
||||
'search_pathnames_only': { queryStr: string, include: string | null, pageNumber: number },
|
||||
'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number },
|
||||
'search_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 },
|
||||
|
|
@ -62,8 +44,9 @@ export type ToolResultType = {
|
|||
'get_dir_structure': { str: string, },
|
||||
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
|
||||
'search_files': { uris: URI[], hasNextPage: boolean },
|
||||
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
|
||||
// ---
|
||||
'edit_file': Promise<{ lintErrorsStr: string | null }>,
|
||||
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
|
||||
'create_file_or_folder': {},
|
||||
'delete_file_or_folder': {},
|
||||
'run_terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; },
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ITextModel } from '../../../../editor/common/model.js';
|
|||
import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
|
||||
|
||||
type VoidModelType = {
|
||||
model: ITextModel | null;
|
||||
|
|
@ -16,6 +17,8 @@ export interface IVoidModelService {
|
|||
getModel(uri: URI): VoidModelType;
|
||||
getModelFromFsPath(fsPath: string): VoidModelType;
|
||||
getModelSafe(uri: URI): Promise<VoidModelType>;
|
||||
saveModel(uri: URI): Promise<void>;
|
||||
|
||||
}
|
||||
|
||||
export const IVoidModelService = createDecorator<IVoidModelService>('voidVoidModelService');
|
||||
|
|
@ -27,10 +30,17 @@ class VoidModelService extends Disposable implements IVoidModelService {
|
|||
|
||||
constructor(
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
saveModel = async (uri: URI) => {
|
||||
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
|
||||
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
|
||||
})
|
||||
}
|
||||
|
||||
initializeModel = async (uri: URI) => {
|
||||
if (uri.fsPath in this._modelRefOfURI) return;
|
||||
const editorModelRef = await this._textModelService.createModelReference(uri);
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
|
|||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { getModelCapabilities } from './modelCapabilities.js';
|
||||
import { defaultProviderSettings, getModelCapabilities } from './modelCapabilities.js';
|
||||
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
// name is the name in the dropdown
|
||||
|
|
@ -71,10 +71,10 @@ export interface IVoidSettingsService {
|
|||
|
||||
|
||||
|
||||
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => {
|
||||
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[] }) => {
|
||||
const { existingModels } = options
|
||||
|
||||
const existingModelsMap: Record<string, VoidModelInfo> = {}
|
||||
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
|
|||
|
||||
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
|
||||
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
|
||||
'Chat': { filter: (o, { chatMode }) => chatMode === 'normal' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } },
|
||||
'Chat': { filter: o => true, emptyMessage: null, },
|
||||
'Ctrl+K': { filter: o => true, emptyMessage: null, },
|
||||
'Apply': { filter: o => true, emptyMessage: null, },
|
||||
}
|
||||
|
|
@ -211,7 +211,15 @@ 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
|
||||
|
|
@ -363,7 +371,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
const modelIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (modelIdx === -1) return
|
||||
const newIsHidden = !models[modelIdx].isHidden
|
||||
const newModels: VoidModelInfo[] = [
|
||||
const newModels: VoidStatefulModelInfo[] = [
|
||||
...models.slice(0, modelIdx),
|
||||
{ ...models[modelIdx], isHidden: newIsHidden },
|
||||
...models.slice(modelIdx + 1, Infinity)
|
||||
|
|
|
|||
|
|
@ -4,52 +4,13 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defaultModelsOfProvider } from './modelCapabilities.js';
|
||||
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
|
||||
import { VoidSettingsState } from './voidSettingsService.js'
|
||||
|
||||
|
||||
type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
|
||||
export const defaultProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAI: {
|
||||
apiKey: '',
|
||||
},
|
||||
deepseek: {
|
||||
apiKey: '',
|
||||
},
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
vLLM: {
|
||||
endpoint: 'http://localhost:8000',
|
||||
},
|
||||
openRouter: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAICompatible: {
|
||||
endpoint: '',
|
||||
apiKey: '',
|
||||
},
|
||||
gemini: {
|
||||
apiKey: '',
|
||||
},
|
||||
groq: {
|
||||
apiKey: '',
|
||||
},
|
||||
xAI: {
|
||||
apiKey: ''
|
||||
},
|
||||
mistral: {
|
||||
apiKey: '',
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
|
||||
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
|
@ -67,7 +28,7 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
|||
|
||||
|
||||
|
||||
export type VoidModelInfo = { // <-- STATEFUL
|
||||
export type VoidStatefulModelInfo = { // <-- STATEFUL
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||
|
|
@ -78,7 +39,7 @@ export type VoidModelInfo = { // <-- STATEFUL
|
|||
|
||||
type CommonProviderSettings = {
|
||||
_didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields
|
||||
models: VoidModelInfo[],
|
||||
models: VoidStatefulModelInfo[],
|
||||
}
|
||||
|
||||
export type SettingsAtProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
|
@ -237,7 +198,7 @@ const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
|||
}
|
||||
|
||||
|
||||
const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidModelInfo[] } => {
|
||||
const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidStatefulModelInfo[] } => {
|
||||
return {
|
||||
models: defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
|
|
@ -350,6 +311,8 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
|
|||
export const refreshableProviderNames = localProviderNames
|
||||
export type RefreshableProviderName = typeof refreshableProviderNames[number]
|
||||
|
||||
// models that come with download buttons
|
||||
export const hasDownloadButtonsOnModelsProviderNames = ['ollama'] as const satisfies ProviderName[]
|
||||
|
||||
|
||||
|
||||
|
|
@ -405,6 +368,9 @@ export type GlobalSettings = {
|
|||
enableFastApply: boolean;
|
||||
chatMode: ChatMode;
|
||||
autoApprove: boolean;
|
||||
showInlineSuggestions: boolean;
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
|
|
@ -415,6 +381,9 @@ export const defaultGlobalSettings: GlobalSettings = {
|
|||
enableFastApply: true,
|
||||
chatMode: 'agent',
|
||||
autoApprove: false,
|
||||
showInlineSuggestions: true,
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: true,
|
||||
}
|
||||
|
||||
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: () => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>;
|
||||
check: (explicit: boolean) => Promise<VoidCheckUpdateRespose>;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -34,8 +35,8 @@ export class VoidUpdateService implements IVoidUpdateService {
|
|||
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
check: IVoidUpdateService['check'] = async () => {
|
||||
const res = await this.voidUpdateService.check()
|
||||
check: IVoidUpdateService['check'] = async (explicit) => {
|
||||
const res = await this.voidUpdateService.check(explicit)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
|
||||
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
|
||||
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
|
||||
import { ChatMode } from '../../common/voidSettingsTypes.js'
|
||||
|
||||
|
||||
// =============== reasoning ===============
|
||||
|
||||
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
|
||||
export const extractReasoningWrapper = (
|
||||
onText: OnText, onFinalMessage: OnFinalMessage, thinkTags: [string, string]
|
||||
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
|
||||
let latestAddIdx = 0 // exclusive index in fullText_
|
||||
let foundTag1 = false
|
||||
let foundTag2 = false
|
||||
|
||||
let fullTextSoFar = ''
|
||||
let fullReasoningSoFar = ''
|
||||
|
||||
let onText_ = onText
|
||||
onText = (params) => {
|
||||
onText_(params)
|
||||
}
|
||||
|
||||
const newOnText: OnText = ({ fullText: fullText_, ...p }) => {
|
||||
|
||||
// until found the first think tag, keep adding to fullText
|
||||
if (!foundTag1) {
|
||||
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
|
||||
if (endsWithTag1) {
|
||||
// console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ })
|
||||
// wait until we get the full tag or know more
|
||||
return
|
||||
}
|
||||
// if found the first tag
|
||||
const tag1Index = fullText_.indexOf(thinkTags[0])
|
||||
if (tag1Index !== -1) {
|
||||
// console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ })
|
||||
foundTag1 = true
|
||||
// Add text before the tag to fullTextSoFar
|
||||
fullTextSoFar += fullText_.substring(0, tag1Index)
|
||||
// Update latestAddIdx to after the first tag
|
||||
latestAddIdx = tag1Index + thinkTags[0].length
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar })
|
||||
// add the text to fullText
|
||||
fullTextSoFar = fullText_
|
||||
latestAddIdx = fullText_.length
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// at this point, we found <tag1>
|
||||
|
||||
// until found the second think tag, keep adding to fullReasoning
|
||||
if (!foundTag2) {
|
||||
const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1])
|
||||
if (endsWithTag2) {
|
||||
// console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar })
|
||||
// wait until we get the full tag or know more
|
||||
return
|
||||
}
|
||||
|
||||
// if found the second tag
|
||||
const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx)
|
||||
if (tag2Index !== -1) {
|
||||
// console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar })
|
||||
foundTag2 = true
|
||||
// Add everything between first and second tag to reasoning
|
||||
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
|
||||
// Update latestAddIdx to after the second tag
|
||||
latestAddIdx = tag2Index + thinkTags[1].length
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// add the text to fullReasoning (content after first tag but before second tag)
|
||||
// console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar })
|
||||
|
||||
// If we have more text than we've processed, add it to reasoning
|
||||
if (fullText_.length > latestAddIdx) {
|
||||
fullReasoningSoFar += fullText_.substring(latestAddIdx)
|
||||
latestAddIdx = fullText_.length
|
||||
}
|
||||
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// at this point, we found <tag2> - content after the second tag is normal text
|
||||
// console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar })
|
||||
|
||||
// Add any new text after the closing tag to fullTextSoFar
|
||||
if (fullText_.length > latestAddIdx) {
|
||||
fullTextSoFar += fullText_.substring(latestAddIdx)
|
||||
latestAddIdx = fullText_.length
|
||||
}
|
||||
|
||||
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
}
|
||||
|
||||
|
||||
const getOnFinalMessageParams = () => {
|
||||
const fullText_ = fullTextSoFar
|
||||
const tag1Idx = fullText_.indexOf(thinkTags[0])
|
||||
const tag2Idx = fullText_.indexOf(thinkTags[1])
|
||||
if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning
|
||||
if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning
|
||||
|
||||
const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx)
|
||||
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
|
||||
|
||||
return { fullText, fullReasoning }
|
||||
}
|
||||
|
||||
const newOnFinalMessage: OnFinalMessage = (params) => {
|
||||
|
||||
// treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage)
|
||||
newOnText({ ...params })
|
||||
|
||||
const { fullText, fullReasoning } = getOnFinalMessageParams()
|
||||
onFinalMessage({ ...params, fullText, fullReasoning })
|
||||
}
|
||||
|
||||
return { newOnText, newOnFinalMessage }
|
||||
}
|
||||
|
||||
|
||||
// =============== tools ===============
|
||||
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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, 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
|
||||
}
|
||||
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 extractToolsWrapper = (
|
||||
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode
|
||||
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
|
||||
const tools = availableTools(chatMode)
|
||||
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
|
||||
const toolOfToolName: ToolOfToolName = {}
|
||||
const toolOpenTags = tools.map(t => `<${t.name}>`)
|
||||
for (const t of tools) { toolOfToolName[t.name] = t }
|
||||
|
||||
// detect <availableTools[0]></availableTools[0]>, etc
|
||||
let fullText = '';
|
||||
let trueFullText = ''
|
||||
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) => {
|
||||
const newText = params.fullText.substring(prevFullTextLen)
|
||||
prevFullTextLen = params.fullText.length
|
||||
trueFullText = params.fullText
|
||||
|
||||
// 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,
|
||||
trueFullText.substring(foundOpenTag.idx, Infinity),
|
||||
toolOfToolName,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
onText({
|
||||
...params,
|
||||
fullText,
|
||||
toolCall: latestToolCall,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const newOnFinalMessage: OnFinalMessage = (params) => {
|
||||
// treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage)
|
||||
newOnText({ ...params })
|
||||
|
||||
fullText = fullText.trimEnd()
|
||||
const toolCall = latestToolCall
|
||||
|
||||
// console.log('final message!!!', trueFullText)
|
||||
// console.log('----- returning ----\n', fullText)
|
||||
// console.log('----- tools ----\n', JSON.stringify(firstToolCallRef.current, null, 2))
|
||||
// console.log('----- toolCall ----\n', JSON.stringify(toolCall, null, 2))
|
||||
|
||||
onFinalMessage({ ...params, fullText, toolCall: toolCall })
|
||||
}
|
||||
return { newOnText, newOnFinalMessage };
|
||||
}
|
||||
|
||||
|
||||
|
||||
// trim all whitespace up until the first newline, and all whitespace up until the last newline
|
||||
const trimBeforeAndAfterNewLines = (s: string) => {
|
||||
if (!s) return s;
|
||||
|
||||
const firstNewLineIndex = s.indexOf('\n');
|
||||
|
||||
if (firstNewLineIndex !== -1 && s.substring(0, firstNewLineIndex).trim() === '') {
|
||||
s = s.substring(firstNewLineIndex + 1, Infinity)
|
||||
}
|
||||
|
||||
const lastNewLineIndex = s.lastIndexOf('\n');
|
||||
if (lastNewLineIndex !== -1 && s.substring(lastNewLineIndex + 1, Infinity).trim() === '') {
|
||||
s = s.substring(0, lastNewLineIndex)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
@ -23,17 +23,10 @@ type InternalLLMChatMessage = {
|
|||
} | {
|
||||
role: 'assistant',
|
||||
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
|
||||
} | {
|
||||
role: 'tool';
|
||||
content: string; // result
|
||||
name: string;
|
||||
params: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
const EMPTY_TOOL_CONTENT = '(empty content)'
|
||||
|
||||
const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }): { messages: LLMChatMessage[] } => {
|
||||
const messages = deepClone(messages_)
|
||||
|
|
@ -145,7 +138,7 @@ const prepareMessages_fitIntoContext = ({ messages, contextWindow, maxOutputToke
|
|||
|
||||
|
||||
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
|
||||
const prepareMessages_systemMessage = ({
|
||||
const prepareMessages_addSystemInstructions = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
|
|
@ -206,193 +199,190 @@ const prepareMessages_systemMessage = ({
|
|||
|
||||
|
||||
|
||||
// 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}"
|
||||
}]
|
||||
// // 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) }
|
||||
// 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
|
||||
*/
|
||||
// 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[], }) => {
|
||||
// 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 = [];
|
||||
// const newMessages: PrepareMessagesToolsOpenAI = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
// for (let i = 0; i < messages.length; i += 1) {
|
||||
// const currMsg = messages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') {
|
||||
newMessages.push(currMsg)
|
||||
continue
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}]
|
||||
}
|
||||
// // 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 }
|
||||
// // 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"
|
||||
}]
|
||||
*/
|
||||
// // 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:
|
||||
// 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;
|
||||
// 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]
|
||||
// for (let i = 0; i < newMessages.length; i += 1) {
|
||||
// const currMsg = newMessages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') continue
|
||||
// if (currMsg.role !== 'tool') continue
|
||||
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
// 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) })
|
||||
}
|
||||
// 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 }
|
||||
}
|
||||
// // 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
|
||||
|
||||
|
||||
|
||||
type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI
|
||||
|
||||
const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | '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`)
|
||||
}
|
||||
}
|
||||
// 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_anthropicContent = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => {
|
||||
const prepareMessages_anthropicReasoning = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => {
|
||||
const newMessages: InternalLLMChatMessage[] = []
|
||||
for (const m of messages) {
|
||||
if (m.role !== 'assistant') {
|
||||
|
|
@ -417,38 +407,18 @@ const prepareMessages_anthropicContent = ({ messages, supportsAnthropicReasoning
|
|||
|
||||
|
||||
// do this at end
|
||||
const prepareMessages_noEmptyMessage = ({ messages }: { messages: PrepareMessagesTools }): { messages: PrepareMessagesTools } => {
|
||||
const prepareMessages_noEmptyMessage = ({ messages }: { messages: InternalLLMChatMessage[] }): { messages: InternalLLMChatMessage[] } => {
|
||||
for (const currMsg of messages) {
|
||||
|
||||
// don't do this for tools
|
||||
if (currMsg.role === 'tool') continue
|
||||
|
||||
// don't do this for assistant or user messages that have tool_calls or tool_results
|
||||
const oai = currMsg as PrepareMessagesToolsOpenAI[0]
|
||||
if (oai.role === 'assistant') {
|
||||
if (oai.tool_calls) continue
|
||||
}
|
||||
const anth = currMsg as PrepareMessagesToolsAnthropic[0]
|
||||
if (anth.role === 'assistant' || anth.role === 'user') {
|
||||
if (typeof anth.content !== 'string') {
|
||||
const hasContent = anth.content.find(c => c.type === 'tool_use' || c.type === 'tool_result')
|
||||
if (hasContent) continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (typeof currMsg.content === 'string') {
|
||||
// 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
|
||||
else if (c.type === 'tool_use') { }
|
||||
else if (c.type === 'tool_result') { }
|
||||
}
|
||||
if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
|
||||
}
|
||||
|
||||
}
|
||||
return { messages }
|
||||
}
|
||||
|
|
@ -461,7 +431,6 @@ export const prepareMessages = ({
|
|||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
supportsAnthropicReasoningSignature,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
|
|
@ -469,7 +438,6 @@ export const prepareMessages = ({
|
|||
messages: LLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
supportsTools: false | 'anthropic-style' | 'openai-style',
|
||||
supportsAnthropicReasoningSignature: boolean,
|
||||
contextWindow: number,
|
||||
maxOutputTokens: number | null | undefined,
|
||||
|
|
@ -478,13 +446,12 @@ export const prepareMessages = ({
|
|||
|
||||
const { messages: messages0 } = prepareMessages_normalize({ messages })
|
||||
const { messages: messages1 } = prepareMessages_fitIntoContext({ messages: messages0, contextWindow, maxOutputTokens })
|
||||
const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicReasoningSignature })
|
||||
const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage })
|
||||
const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools })
|
||||
const { messages: messages5 } = prepareMessages_noEmptyMessage({ messages: messages4 })
|
||||
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: messages5 as any,
|
||||
messages: messages4 as any,
|
||||
separateSystemMessageStr
|
||||
} as const
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ import { MistralCore } from "@mistralai/mistralai/core.js";
|
|||
import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js";
|
||||
|
||||
|
||||
import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js';
|
||||
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js';
|
||||
import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js';
|
||||
import { InternalToolInfo, ToolName, isAToolName } from '../../common/toolsServiceTypes.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings } from '../../common/modelCapabilities.js';
|
||||
import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js';
|
||||
|
||||
|
||||
type InternalCommonMessageParams = {
|
||||
|
|
@ -30,7 +29,7 @@ type InternalCommonMessageParams = {
|
|||
_setAborter: (aborter: () => void) => void;
|
||||
}
|
||||
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; tools?: InternalToolInfo[] }
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; }
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
|
||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||
|
||||
|
|
@ -38,34 +37,6 @@ export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
|||
const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayInfoOfProviderName(providerName).title} API key.`
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
||||
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
|
||||
}
|
||||
|
||||
type ToolCallOfIndex = { [index: string]: { name: string, paramsStr: string, id: string } } // type used to stream tool calls as they come in
|
||||
type ToolCallsFrom_ReturnType = { name: ToolName, id: string, paramsStr: string }[] // return type of toolCallsFrom_<PROVIDER>
|
||||
|
||||
const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex): ToolCallsFrom_ReturnType => {
|
||||
return Object.keys(toolCallOfIndex).map(index => {
|
||||
const tool = toolCallOfIndex[index]
|
||||
return isAToolName(tool.name) ? { name: tool.name, id: tool.id, paramsStr: tool.paramsStr } : null
|
||||
}).filter(t => !!t)
|
||||
}
|
||||
|
||||
|
||||
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
const commonPayloadOpts: ClientOptions = {
|
||||
|
|
@ -98,7 +69,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
|||
}
|
||||
else if (providerName === 'gemini') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
|
|
@ -159,11 +130,10 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
|
|||
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
|
||||
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, chatMode }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
reasoningCapabilities,
|
||||
|
|
@ -176,55 +146,44 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
// tools
|
||||
const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined
|
||||
const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {}
|
||||
|
||||
// max tokens
|
||||
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
|
||||
// instance
|
||||
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens })
|
||||
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,
|
||||
stream: true,
|
||||
// max_completion_tokens: maxTokens,
|
||||
...toolsObj,
|
||||
}
|
||||
|
||||
// open source models - manually parse think tokens
|
||||
const { needsManualParse: needsManualReasoningParse, nameOfFieldInDelta: nameOfReasoningFieldInDelta } = providerReasoningIOSettings?.output ?? {}
|
||||
const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags
|
||||
if (manuallyParseReasoning) {
|
||||
onText = extractReasoningOnTextWrapper(onText, openSourceThinkTags)
|
||||
const { newOnText, newOnFinalMessage } = extractReasoningWrapper(onText, onFinalMessage, openSourceThinkTags)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
|
||||
// manually parse out tool results
|
||||
if (chatMode) {
|
||||
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
|
||||
let fullReasoningSoFar = ''
|
||||
let fullTextSoFar = ''
|
||||
|
||||
let fullToolName = ''
|
||||
let fullToolParams = ''
|
||||
|
||||
const toolCallOfIndex: ToolCallOfIndex = {}
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
// tool call
|
||||
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
|
||||
const index = tool.index
|
||||
if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', paramsStr: '', id: '' }
|
||||
toolCallOfIndex[index].name += tool.function?.name ?? ''
|
||||
toolCallOfIndex[index].paramsStr += tool.function?.arguments ?? '';
|
||||
toolCallOfIndex[index].id += tool.id ?? ''
|
||||
|
||||
fullToolName += tool.function?.name ?? ''
|
||||
fullToolParams += tool.function?.arguments ?? ''
|
||||
}
|
||||
// message
|
||||
const newText = chunk.choices[0]?.delta?.content ?? ''
|
||||
fullTextSoFar += newText
|
||||
|
|
@ -237,20 +196,14 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
fullReasoningSoFar += newReasoning
|
||||
}
|
||||
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, fullToolName, fullToolParams })
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
}
|
||||
// on final
|
||||
const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex)
|
||||
if (!fullTextSoFar && !fullReasoningSoFar && toolCalls.length === 0) {
|
||||
if (!fullTextSoFar && !fullReasoningSoFar) {
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
}
|
||||
else {
|
||||
if (manuallyParseReasoning) {
|
||||
const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, openSourceThinkTags)
|
||||
onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning: null });
|
||||
} else {
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls, anthropicReasoning: null });
|
||||
}
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null });
|
||||
}
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
|
|
@ -299,33 +252,11 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
|
|||
|
||||
|
||||
// ------------ ANTHROPIC ------------
|
||||
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 toolCallsFrom_Anthropic = (content: Anthropic.Messages.ContentBlock[]): ToolCallsFrom_ReturnType => {
|
||||
return content.map(c => {
|
||||
if (c.type !== 'tool_use') return null
|
||||
if (!isAToolName(c.name)) return null
|
||||
return c.type === 'tool_use' ? { name: c.name, paramsStr: JSON.stringify(c.input), id: c.id } : null
|
||||
}).filter(t => !!t)
|
||||
}
|
||||
|
||||
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
|
||||
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, chatMode }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
supportsSystemMessage,
|
||||
contextWindow,
|
||||
supportsTools,
|
||||
maxOutputTokens,
|
||||
reasoningCapabilities,
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
|
|
@ -337,18 +268,11 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
|||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
// tools
|
||||
const tools = ((tools_?.length ?? 0) !== 0) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined
|
||||
const toolsObj: Partial<Anthropic.Messages.MessageStreamParams> = tools ? {
|
||||
tools: tools,
|
||||
tool_choice: { type: 'auto', disable_parallel_tool_use: true } // one tool at a time
|
||||
} : {}
|
||||
|
||||
// anthropic-specific - max tokens
|
||||
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
|
||||
// instance
|
||||
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
|
||||
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: thisConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
|
|
@ -359,16 +283,22 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
|||
messages: messages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens ?? 4_096, // anthropic requires this
|
||||
...toolsObj,
|
||||
...includeInPayload,
|
||||
})
|
||||
|
||||
// manually parse out tool results
|
||||
if (chatMode) {
|
||||
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
|
||||
// when receive text
|
||||
let fullText = ''
|
||||
let fullReasoning = ''
|
||||
|
||||
let fullToolName = ''
|
||||
let fullToolParams = ''
|
||||
// let fullToolName = ''
|
||||
// let fullToolParams = ''
|
||||
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
stream.on('streamEvent', e => {
|
||||
|
|
@ -377,47 +307,46 @@ 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, fullToolName, fullToolParams })
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
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, fullToolName, fullToolParams })
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
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, fullToolName, fullToolParams })
|
||||
}
|
||||
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, fullToolName, fullToolParams })
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
// 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, fullToolName, fullToolParams })
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
else if (e.delta.type === 'thinking_delta') {
|
||||
fullReasoning += e.delta.thinking
|
||||
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
|
||||
}
|
||||
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, fullToolName, fullToolParams })
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
// 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 toolCalls = toolCallsFrom_Anthropic(response.content)
|
||||
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
|
||||
onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning })
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning })
|
||||
})
|
||||
// on error
|
||||
stream.on('error', (error) => {
|
||||
|
|
@ -464,22 +393,6 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings
|
|||
})
|
||||
}
|
||||
|
||||
// // in future, can do tool_use streaming in anthropic, but it's pretty fast even without streaming...
|
||||
// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
|
||||
// stream.on('streamEvent', e => {
|
||||
// if (e.type === 'content_block_start') {
|
||||
// if (e.content_block.type !== 'tool_use') return
|
||||
// const index = e.index
|
||||
// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
|
||||
// toolCallOfIndex[index].name += e.content_block.name ?? ''
|
||||
// toolCallOfIndex[index].args += e.content_block.input ?? ''
|
||||
// }
|
||||
// else if (e.type === 'content_block_delta') {
|
||||
// if (e.delta.type !== 'input_json_delta') return
|
||||
// toolCallOfIndex[e.index].args += e.delta.partial_json
|
||||
// }
|
||||
// })
|
||||
|
||||
|
||||
// ------------ OLLAMA ------------
|
||||
const newOllamaSDK = ({ endpoint }: { endpoint: string }) => {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const sendLLMMessage = ({
|
|||
settingsOfProvider,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
tools,
|
||||
chatMode,
|
||||
}: SendLLMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
|
|
@ -108,7 +108,7 @@ export const sendLLMMessage = ({
|
|||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, tools })
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, chatMode })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@
|
|||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
|
||||
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,33 +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,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async check() {
|
||||
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
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
|
||||
const resJSON = await res.json()
|
||||
this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves
|
||||
|
||||
if (!resJSON) return null
|
||||
console.log('updateState', this._updateService.state)
|
||||
|
||||
const { hasUpdate, downloadMessage } = resJSON ?? {}
|
||||
if (hasUpdate === undefined)
|
||||
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 ? 'Currently checking for updates...' : 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) {
|
||||
// Update is ready
|
||||
return { message: 'Restart Void to update!', action: 'restart' } 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()
|
||||
|
||||
if (!resJSON) return null // null means error
|
||||
|
||||
const { hasUpdate, downloadMessage } = resJSON ?? {}
|
||||
if (hasUpdate === undefined)
|
||||
return null
|
||||
|
||||
const after = (downloadMessage || '') + ''
|
||||
if (hasUpdate)
|
||||
return { message: after, action: 'reinstall' } as const
|
||||
return { message: 'Void is up-to-date!' } as const
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
const after = (downloadMessage || '') + ''
|
||||
return { hasUpdate: !!hasUpdate, message: after }
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue