diff --git a/CONTRIBUTING.md b/HOW_TO_CONTRIBUTE.md similarity index 94% rename from CONTRIBUTING.md rename to HOW_TO_CONTRIBUTE.md index 26e6d681..d2a1e649 100644 --- a/CONTRIBUTING.md +++ b/HOW_TO_CONTRIBUTE.md @@ -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! - +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/`. diff --git a/README.md b/README.md index 609456cb..56bcd1ce 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/VOID_CODEBASE_GUIDE.md b/VOID_CODEBASE_GUIDE.md new file mode 100644 index 00000000..a6b01e52 --- /dev/null +++ b/VOID_CODEBASE_GUIDE.md @@ -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 `@`. 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. + + +
+ +
+ + + +**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: + +
+ +
+ +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: + +
+ +
+ + + +### 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). + + diff --git a/VOID_USEFUL_LINKS.md b/VOID_USEFUL_LINKS.md deleted file mode 100644 index 3fcfe797..00000000 --- a/VOID_USEFUL_LINKS.md +++ /dev/null @@ -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). - - diff --git a/build/gulpfile.compile.js b/build/gulpfile.compile.js index 0c0a024c..e40b05f8 100644 --- a/build/gulpfile.compile.js +++ b/build/gulpfile.compile.js @@ -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; diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 125a405b..b4b7f49e 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -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, diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 399fa243..f34954fa 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -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'; diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index d2b49929..02b17022 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -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); diff --git a/package-lock.json b/package-lock.json index 0e6f871e..270393d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 059e521f..e7613ff0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/bootstrap-fork.ts b/src/bootstrap-fork.ts index a92290a2..d9f424af 100644 --- a/src/bootstrap-fork.ts +++ b/src/bootstrap-fork.ts @@ -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'; diff --git a/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts b/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts index 520e71d2..931f2dc5 100644 --- a/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts +++ b/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts @@ -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); diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index e940f626..603515a9 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -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; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 16d645a8..39492ef3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -612,7 +612,7 @@ const registry = Registry.as(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': { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 2dea3290..365ad73a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -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() { diff --git a/src/vs/workbench/contrib/void/browser/_dummyContrib.ts b/src/vs/workbench/contrib/void/browser/_dummyContrib.ts index e19c1182..6898845e 100644 --- a/src/vs/workbench/contrib/void/browser/_dummyContrib.ts +++ b/src/vs/workbench/contrib/void/browser/_dummyContrib.ts @@ -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('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); diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index b8c6c466..e1832646 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -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 { 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 }); }, - }) + })) } diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 4a2257a3..ac4608c6 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -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 = 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` + + 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 + 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 & { 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((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((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 diff --git a/src/vs/workbench/contrib/void/browser/directoryStrService.ts b/src/vs/workbench/contrib/void/browser/directoryStrService.ts index 4cdb8f00..ff5972bc 100644 --- a/src/vs/workbench/contrib/void/browser/directoryStrService.ts +++ b/src/vs/workbench/contrib/void/browser/directoryStrService.ts @@ -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 + getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise } export const IDirectoryStrService = createDecorator('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 => { - // 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 } } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2b4d2eae..efd70d7e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -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) } diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css index 3a420737..ac5ff5f3 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -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; } diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js index 26b5bc37..9507aa59 100755 --- a/src/vs/workbench/contrib/void/browser/react/build.js +++ b/src/vs/workbench/contrib/void/browser/react/build.js @@ -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); diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index c2ef204e..9f364ba0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -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) => ( @@ -94,13 +93,14 @@ export const CopyButton = ({ codeStr }: { codeStr: string }) => { return } -export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => { +export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & React.ButtonHTMLAttributes) => { 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 }) => { ) } @@ -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) => { + return ( +
+ {title && {title}} +
+
+ ); +}; + +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) => { + const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) - return
-
-
+ 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 = + 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 + return } if (currStreamState === 'idle-no-changes') { - return + + return } if (currStreamState === 'idle-has-changes') { @@ -267,19 +322,18 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co } } - export const BlockCodeApplyWrapper = ({ children, initValue, @@ -314,7 +368,7 @@ export const BlockCodeApplyWrapper = ({ {/* header */}
- + {name} diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index d541dc43..2a531815 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -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(null) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index bad7832e..6ae68be1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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 = ({
-
+
{featureName === 'Chat' && }
@@ -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} > @@ -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 (
-
+
{/* header */} -
{ - if (isDropdown) { setIsOpen(v => !v); } - if (onClick) { onClick(); } - }} - > - {isDropdown && ( - - )} +
{/* left */} -
+
{ + if (isDropdown) { setIsOpen(v => !v); } + if (onClick) { onClick(); } + }} + > + {isDropdown && ()} {title} - {desc1} + {desc1}
{/* right */} @@ -724,6 +737,7 @@ const ToolHeaderWrapper = ({ {children}
}
+ {bottomChildren}
); }; @@ -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 } - + const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 return
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -952,25 +966,32 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
- { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() - } - }} - /> + +
+ { + if (mode === 'display') { + onOpenEdit() + } else if (mode === 'edit') { + onCloseEdit() + } + }} + /> +
+
@@ -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
{ +) + +export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( + +) + + + 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 = + + + // ======== 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 =
+ { + 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' + /> + + { + 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' + /> + + + +
+ + + // !select-text cursor-auto + const fileDetailsContent =
+ {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 =
commandService.executeCommand('vscode.open', uri, { preview: true })} + > + {/* */} + {basename} +
+ + + + + const detailsContent =
+ {numDiffs} diff{numDiffs !== 1 ? 's' : ''} +
+ + const acceptRejectButtons =
+ + { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "reject", _addToHistory: true, }); }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Reject file' + + /> + { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "accept", _addToHistory: true, }); }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Accept file' + /> + +
+ + const fileStatusHTML = + + return ( + // name, details +
+
+ {fileNameHTML} + {detailsContent} +
+
+ {acceptRejectButtons} + {fileStatusHTML} +
+
+ ) + })} +
+ + const fileDetailsButton = ( + + ) return ( - - {sortedCommandBarURIs.map((uri, i) => ( - { commandService.executeCommand('vscode.open', uri, { preview: true }) }} - /> - ))} - + <> + {/* file details */} +
+
+ {fileDetailsContent} +
+
+ {/* main content */} +
+
+ {fileDetailsButton} +
+
+ {acceptRejectAllButtons} + {threadStatusHTML} +
+
+ ) } +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 = + {uriDone ? + getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown') + : `Generating`} + + + + // If URI has not been specified + return } + > + + + + + + +} + export const SidebarChat = () => { const textAreaRef = useRef(null) const textAreaFnsRef = useRef(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(null) const scrollContainerRef = useRef(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 { _scrollToBottom={() => scrollToBottom(scrollContainerRef)} /> }) - }, [previousMessages, isRunning, threadId]) + }, [previousMessages, threadId, currCheckpointIdx, isRunning]) const streamingChatIdx = previousMessagesHTML.length - const currStreamingMessageHTML = reasoningSoFar || messageSoFar || isRunning ? + const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ? { /> : 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' ? + : null + : null const messagesHTML = { 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 ? - Generating} /> - : null} - + {/* loading indicator */} {isRunning === 'LLM' && !toolIsGenerating ? - {/* loading indicator */} {} : null} @@ -2159,33 +2511,40 @@ export const SidebarChat = () => { } }, [onSubmit, onAbort, isRunning]) - const inputForm =
- { textAreaRef.current?.focus() }} + const inputForm =
+
+ {previousMessages.length > 0 && + + } +
+
- { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} - ref={textAreaRef} - fnsRef={textAreaFnsRef} - multiline={true} - /> + { textAreaRef.current?.focus() }} + > + { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} + ref={textAreaRef} + fnsRef={textAreaFnsRef} + multiline={true} + /> - + +
return ( diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 7db16221..96909236 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -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 (
  • diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 8e82d750..b7e31757 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -106,6 +106,7 @@ export const VoidInputBox2 = forwardRef(fun return (