From 79fb8f63a75e111bf4fc3957610dc620d816ccc5 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina <75667013+bjoaquinc@users.noreply.github.com> Date: Thu, 13 Feb 2025 01:33:43 +0700 Subject: [PATCH 01/92] Added Open Remote-SSH extension to Void --- build/gulpfile.extensions.js | 1 + extensions/open-remote-ssh/CHANGELOG.md | 74 +++ extensions/open-remote-ssh/README.md | 48 ++ .../extension-browser.webpack.config.js | 17 + .../extension.webpack.config.js | 34 + extensions/open-remote-ssh/package-lock.json | 370 +++++++++++ extensions/open-remote-ssh/package.json | 351 ++++++++++ extensions/open-remote-ssh/resources/icon.png | Bin 0 -> 17422 bytes .../open-remote-ssh/src/authResolver.ts | 464 +++++++++++++ extensions/open-remote-ssh/src/commands.ts | 68 ++ .../open-remote-ssh/src/common/disposable.ts | 41 ++ .../open-remote-ssh/src/common/files.ts | 25 + .../open-remote-ssh/src/common/logger.ts | 63 ++ .../open-remote-ssh/src/common/platform.ts | 7 + .../open-remote-ssh/src/common/ports.ts | 133 ++++ extensions/open-remote-ssh/src/extension.ts | 37 ++ .../open-remote-ssh/src/hostTreeView.ts | 109 +++ .../src/remoteLocationHistory.ts | 58 ++ .../open-remote-ssh/src/serverConfig.ts | 43 ++ extensions/open-remote-ssh/src/serverSetup.ts | 626 ++++++++++++++++++ .../open-remote-ssh/src/ssh/hostfile.ts | 46 ++ .../open-remote-ssh/src/ssh/identityFiles.ts | 120 ++++ .../open-remote-ssh/src/ssh/sshConfig.ts | 129 ++++ .../open-remote-ssh/src/ssh/sshConnection.ts | 367 ++++++++++ .../open-remote-ssh/src/ssh/sshDestination.ts | 58 ++ extensions/open-remote-ssh/tsconfig.json | 12 + 26 files changed, 3301 insertions(+) create mode 100644 extensions/open-remote-ssh/CHANGELOG.md create mode 100644 extensions/open-remote-ssh/README.md create mode 100644 extensions/open-remote-ssh/extension-browser.webpack.config.js create mode 100644 extensions/open-remote-ssh/extension.webpack.config.js create mode 100644 extensions/open-remote-ssh/package-lock.json create mode 100644 extensions/open-remote-ssh/package.json create mode 100644 extensions/open-remote-ssh/resources/icon.png create mode 100644 extensions/open-remote-ssh/src/authResolver.ts create mode 100644 extensions/open-remote-ssh/src/commands.ts create mode 100644 extensions/open-remote-ssh/src/common/disposable.ts create mode 100644 extensions/open-remote-ssh/src/common/files.ts create mode 100644 extensions/open-remote-ssh/src/common/logger.ts create mode 100644 extensions/open-remote-ssh/src/common/platform.ts create mode 100644 extensions/open-remote-ssh/src/common/ports.ts create mode 100644 extensions/open-remote-ssh/src/extension.ts create mode 100644 extensions/open-remote-ssh/src/hostTreeView.ts create mode 100644 extensions/open-remote-ssh/src/remoteLocationHistory.ts create mode 100644 extensions/open-remote-ssh/src/serverConfig.ts create mode 100644 extensions/open-remote-ssh/src/serverSetup.ts create mode 100644 extensions/open-remote-ssh/src/ssh/hostfile.ts create mode 100644 extensions/open-remote-ssh/src/ssh/identityFiles.ts create mode 100644 extensions/open-remote-ssh/src/ssh/sshConfig.ts create mode 100644 extensions/open-remote-ssh/src/ssh/sshConnection.ts create mode 100644 extensions/open-remote-ssh/src/ssh/sshDestination.ts create mode 100644 extensions/open-remote-ssh/tsconfig.json diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 4f745aeb..65ea5daf 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -55,6 +55,7 @@ const compilations = [ 'extensions/microsoft-authentication/tsconfig.json', 'extensions/notebook-renderers/tsconfig.json', 'extensions/npm/tsconfig.json', + 'extensions/open-remote-ssh/tsconfig.json', 'extensions/php-language-features/tsconfig.json', 'extensions/references-view/tsconfig.json', 'extensions/search-result/tsconfig.json', diff --git a/extensions/open-remote-ssh/CHANGELOG.md b/extensions/open-remote-ssh/CHANGELOG.md new file mode 100644 index 00000000..71a79fd2 --- /dev/null +++ b/extensions/open-remote-ssh/CHANGELOG.md @@ -0,0 +1,74 @@ +## 0.0.48 +- Support `%n` in ProxyCommand +- fix: add missing direct @types/ssh2-stream dependency (#177) +- fix Win32 internal error (#178) + +## 0.0.47 +- Add support for loong64 (#175) +- Add s390x support (#174) +- Support vscodium alpine reh (#142) + +## 0.0.46 +- Add riscv64 support (#147) + +## 0.0.45 +- Use windows-x64 server on windows-arm64 + +## 0.0.44 +- Update ssh2 lib +- Properly set extensionHost env variables + +## 0.0.43 +- Fix parsing multiple include directives + +## 0.0.42 +- Fix remote label to show port when connecting to a port other than 22 + +## 0.0.41 +- Take into account parsed port from ssh destination. Fixes (#110) + +## 0.0.40 +- Update ssh-config package + +## 0.0.39 + +- output error messages when downloading vscode server (#39) +- Add PreferredAuthentications support (#97) + +## 0.0.38 + +- Enable remote support for ppc64le (#93) + +## 0.0.37 + +- Default to Current OS User in Connection String if No User Provided (#91) +- Add support for (unofficial) DragonFly reh (#86) + +## 0.0.36 + +- Make wget support continue download (#85) + +## 0.0.35 + +- Fixes hardcoded agentsock for windows breaks pageant compatibility (#81) + +## 0.0.34 + +- Add remote.SSH.connectTimeout setting +- adding %r username replacement to proxycommand (#77) + +## 0.0.33 + +- feat: support %r user substitution in proxycommand + +## 0.0.32 + +- feat: use serverDownloadUrlTemplate from product.json (#59) + +## 0.0.31 + +- feat: support glob patterns in SSH include directives + +## 0.0.30 + +- feat: support file patterns in SSH include directives diff --git a/extensions/open-remote-ssh/README.md b/extensions/open-remote-ssh/README.md new file mode 100644 index 00000000..2a208f68 --- /dev/null +++ b/extensions/open-remote-ssh/README.md @@ -0,0 +1,48 @@ +# Open Remote - SSH + +## SSH Host Requirements + +You can connect to a running SSH server on the following platforms. + +**Supported**: + +- x86_64 Debian 8+, Ubuntu 16.04+, CentOS / RHEL 7+ Linux. +- ARMv7l (AArch32) Raspbian Stretch/9+ (32-bit). +- ARMv8l (AArch64) Ubuntu 18.04+ (64-bit). +- macOS 10.14+ (Mojave) +- Windows 10+ +- FreeBSD 13 (Requires manual remote-extension-host installation) +- DragonFlyBSD (Requires manual remote-extension-host installation) + +## Requirements + +**Activation** + +Enable the extension in your `argv.json` + +```json +{ + ... + "enable-proposed-api": [ + ..., + "jeanp413.open-remote-ssh", + ] + ... +} +``` + +which you can open by running the `Preferences: Configure Runtime Arguments` command. +The file is located in `~/.vscode-oss/argv.json`. + +**Alpine linux** + +When running on alpine linux, the packages `libstdc++` and `bash` are necessary and can be installed via +running + +```bash +sudo apk add bash libstdc++ +``` + +## SSH configuration file + +[OpenSSH](https://www.openssh.com/) supports using a [configuration file](https://linuxize.com/post/using-the-ssh-config-file/) to store all your different SSH connections. To use an SSH config file, run the `Remote-SSH: Open SSH Configuration File...` command. diff --git a/extensions/open-remote-ssh/extension-browser.webpack.config.js b/extensions/open-remote-ssh/extension-browser.webpack.config.js new file mode 100644 index 00000000..7fcc53a7 --- /dev/null +++ b/extensions/open-remote-ssh/extension-browser.webpack.config.js @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withBrowserDefaults = require('../shared.webpack.config').browser; + +module.exports = withBrowserDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts' + } +}); diff --git a/extensions/open-remote-ssh/extension.webpack.config.js b/extensions/open-remote-ssh/extension.webpack.config.js new file mode 100644 index 00000000..e124b7f4 --- /dev/null +++ b/extensions/open-remote-ssh/extension.webpack.config.js @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); +const { IgnorePlugin } = require('webpack'); + +module.exports = withDefaults({ + context: __dirname, + resolve: { + mainFields: ['module', 'main'] + }, + entry: { + extension: './src/extension.ts', + }, + externals: { + vscode: "commonjs vscode", + bufferutil: "commonjs bufferutil", + "utf-8-validate": "commonjs utf-8-validate", + }, + plugins: [ + new IgnorePlugin({ + resourceRegExp: /crypto\/build\/Release\/sshcrypto\.node$/, + }), + new IgnorePlugin({ + resourceRegExp: /cpu-features/, + }) + ] +}); diff --git a/extensions/open-remote-ssh/package-lock.json b/extensions/open-remote-ssh/package-lock.json new file mode 100644 index 00000000..8b9b7d82 --- /dev/null +++ b/extensions/open-remote-ssh/package-lock.json @@ -0,0 +1,370 @@ +{ + "name": "open-remote-ssh", + "version": "0.0.48", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "open-remote-ssh", + "version": "0.0.48", + "dependencies": { + "@jeanp413/ssh-config": "^4.3.1", + "glob": "^9.3.1", + "simple-socks": "git+https://github.com/jeanp413/simple-socks#main", + "socks": "^2.5.0", + "ssh2": "git+https://github.com/jeanp413/ssh2#master" + }, + "devDependencies": { + "@types/ssh2": "^0.5.52", + "@types/ssh2-streams": "0.1.12" + }, + "engines": { + "vscode": "^1.70.2" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.0.tgz", + "integrity": "sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jeanp413/ssh-config": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@jeanp413/ssh-config/-/ssh-config-4.3.1.tgz", + "integrity": "sha512-x0EaWRdjs5sPDNmYr11wVB1GdwWQgRekc7SbueuO5FK7YZUav98qZKtZZU5iSDKyxJkooCs3rgVizB1wIWrF7g==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz", + "integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/core-js-pure": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.40.0.tgz", + "integrity": "sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/simple-socks": { + "version": "2.2.2", + "resolved": "git+ssh://git@github.com/jeanp413/simple-socks.git#2ac739301a82d6baff04804ed494436a026acb60", + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.16.8", + "binary": "^0.3.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/ssh2": { + "version": "1.14.0", + "resolved": "git+ssh://git@github.com/jeanp413/ssh2.git#a169f627213aa663e0aa2fd2f0ef5c8931890c26", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.9", + "nan": "^2.17.0" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/extensions/open-remote-ssh/package.json b/extensions/open-remote-ssh/package.json new file mode 100644 index 00000000..f6fc75a8 --- /dev/null +++ b/extensions/open-remote-ssh/package.json @@ -0,0 +1,351 @@ +{ + "name": "open-remote-ssh", + "displayName": "Open Remote - SSH", + "publisher": "voideditor", + "description": "Use any remote machine with a SSH server as your development environment.", + "version": "0.0.48", + "icon": "resources/icon.png", + "engines": { + "vscode": "^1.70.2" + }, + "extensionKind": [ + "ui" + ], + "enabledApiProposals": [ + "resolvers", + "contribViewsRemote" + ], + "keywords": [ + "remote development", + "remote", + "ssh" + ], + "api": "none", + "activationEvents": [ + "onCommand:openremotessh.openEmptyWindow", + "onCommand:openremotessh.openEmptyWindowInCurrentWindow", + "onCommand:openremotessh.openConfigFile", + "onCommand:openremotessh.showLog", + "onResolveRemoteAuthority:ssh-remote", + "onView:sshHosts" + ], + "main": "./out/extension.js", + "contributes": { + "configuration": { + "title": "Remote - SSH", + "properties": { + "remote.SSH.configFile": { + "type": "string", + "description": "The absolute file path to a custom SSH config file.", + "default": "", + "scope": "application" + }, + "remote.SSH.connectTimeout": { + "type": "number", + "description": "Specifies the timeout in seconds used for the SSH command that connects to the remote.", + "default": 60, + "scope": "application", + "minimum": 1 + }, + "remote.SSH.defaultExtensions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of extensions that should be installed automatically on all SSH hosts.", + "scope": "application" + }, + "remote.SSH.enableDynamicForwarding": { + "type": "boolean", + "description": "Whether to use SSH dynamic forwarding to allow setting up new port tunnels over an existing SSH connection.", + "scope": "application", + "default": true + }, + "remote.SSH.enableAgentForwarding": { + "type": "boolean", + "markdownDescription": "Enable fixing the remote environment so that the SSH config option `ForwardAgent` will take effect as expected from VS Code's remote extension host.", + "scope": "application", + "default": true + }, + "remote.SSH.serverDownloadUrlTemplate": { + "type": "string", + "description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number", + "scope": "application", + "default": "https://github.com/voideditor/${NAME_OF_REPO}/releases/download/${version}.${release}/void-server-${os}-${arch}-${version}.${release}.tar.gz" + }, + "remote.SSH.remotePlatform": { + "type": "object", + "description": "A map of the remote hostname to the platform for that remote. Valid values: linux, macos, windows.", + "scope": "application", + "default": {}, + "additionalProperties": { + "type": "string", + "enum": [ + "linux", + "macos", + "windows" + ] + } + }, + "remote.SSH.remoteServerListenOnSocket": { + "type": "boolean", + "description": "When true, the remote vscode server will listen on a socket path instead of opening a port. Only valid for Linux and macOS remotes. Requires `AllowStreamLocalForwarding` to be enabled for the SSH server.", + "default": false + }, + "remote.SSH.experimental.serverBinaryName": { + "type": "string", + "description": "**Experimental:** The name of the server binary, use this **only if** you are using a client without a corresponding server release", + "scope": "application", + "default": "" + } + } + }, + "views": { + "remote": [ + { + "id": "sshHosts", + "name": "SSH Targets", + "group": "targets@1", + "remoteName": "ssh-remote" + } + ] + }, + "commands": [ + { + "command": "openremotessh.openEmptyWindow", + "title": "Connect to Host...", + "category": "Remote-SSH" + }, + { + "command": "openremotessh.openEmptyWindowInCurrentWindow", + "title": "Connect Current Window to Host...", + "category": "Remote-SSH" + }, + { + "command": "openremotessh.openConfigFile", + "title": "Open SSH Configuration File...", + "category": "Remote-SSH" + }, + { + "command": "openremotessh.showLog", + "title": "Show Log", + "category": "Remote-SSH" + }, + { + "command": "openremotessh.explorer.emptyWindowInNewWindow", + "title": "Connect to Host in New Window", + "icon": "$(empty-window)" + }, + { + "command": "openremotessh.explorer.emptyWindowInCurrentWindow", + "title": "Connect to Host in Current Window" + }, + { + "command": "openremotessh.explorer.reopenFolderInCurrentWindow", + "title": "Open on SSH Host in Current Window" + }, + { + "command": "openremotessh.explorer.reopenFolderInNewWindow", + "title": "Open on SSH Host in New Window", + "icon": "$(folder-opened)" + }, + { + "command": "openremotessh.explorer.deleteFolderHistoryItem", + "title": "Remove From Recent List", + "icon": "$(x)" + }, + { + "command": "openremotessh.explorer.refresh", + "title": "Refresh", + "icon": "$(refresh)" + }, + { + "command": "openremotessh.explorer.configure", + "title": "Configure", + "icon": "$(gear)" + }, + { + "command": "openremotessh.explorer.add", + "title": "Add New", + "icon": "$(plus)" + } + ], + "resourceLabelFormatters": [ + { + "scheme": "vscode-remote", + "authority": "ssh-remote+*", + "formatting": { + "label": "${path}", + "separator": "/", + "tildify": true, + "workspaceSuffix": "SSH" + } + } + ], + "menus": { + "statusBar/remoteIndicator": [ + { + "command": "openremotessh.openEmptyWindow", + "when": "remoteName =~ /^ssh-remote$/ && remoteConnectionState == connected", + "group": "remote_20_ssh_1general@1" + }, + { + "command": "openremotessh.openEmptyWindowInCurrentWindow", + "when": "remoteName =~ /^ssh-remote$/ && remoteConnectionState == connected", + "group": "remote_20_ssh_1general@2" + }, + { + "command": "openremotessh.openConfigFile", + "when": "remoteName =~ /^ssh-remote$/ && remoteConnectionState == connected", + "group": "remote_20_ssh_1general@3" + }, + { + "command": "openremotessh.showLog", + "when": "remoteName =~ /^ssh-remote$/ && remoteConnectionState == connected", + "group": "remote_20_ssh_1general@4" + }, + { + "command": "openremotessh.openEmptyWindow", + "when": "remoteConnectionState == disconnected", + "group": "remote_20_ssh_3local@1" + }, + { + "command": "openremotessh.openEmptyWindowInCurrentWindow", + "when": "remoteConnectionState == disconnected", + "group": "remote_20_ssh_3local@2" + }, + { + "command": "openremotessh.openConfigFile", + "when": "remoteConnectionState == disconnected", + "group": "remote_20_ssh_3local@3" + }, + { + "command": "openremotessh.openEmptyWindow", + "when": "!remoteName && !virtualWorkspace", + "group": "remote_20_ssh_3local@5" + }, + { + "command": "openremotessh.openEmptyWindowInCurrentWindow", + "when": "!remoteName && !virtualWorkspace", + "group": "remote_20_ssh_3local@6" + }, + { + "command": "openremotessh.openConfigFile", + "when": "!remoteName && !virtualWorkspace", + "group": "remote_20_ssh_3local@7" + } + ], + "commandPalette": [ + { + "command": "openremotessh.explorer.refresh", + "when": "false" + }, + { + "command": "openremotessh.explorer.configure", + "when": "false" + }, + { + "command": "openremotessh.explorer.add", + "when": "false" + }, + { + "command": "openremotessh.explorer.emptyWindowInNewWindow", + "when": "false" + }, + { + "command": "openremotessh.explorer.emptyWindowInCurrentWindow", + "when": "false" + }, + { + "command": "openremotessh.explorer.reopenFolderInCurrentWindow", + "when": "false" + }, + { + "command": "openremotessh.explorer.reopenFolderInNewWindow", + "when": "false" + }, + { + "command": "openremotessh.explorer.deleteFolderHistoryItem", + "when": "false" + } + ], + "view/title": [ + { + "command": "openremotessh.explorer.add", + "when": "view == sshHosts", + "group": "navigation" + }, + { + "command": "openremotessh.explorer.configure", + "when": "view == sshHosts", + "group": "navigation" + }, + { + "command": "openremotessh.explorer.refresh", + "when": "view == sshHosts", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "openremotessh.explorer.emptyWindowInNewWindow", + "when": "viewItem =~ /^openremotessh.explorer.host$/", + "group": "inline@1" + }, + { + "command": "openremotessh.explorer.emptyWindowInNewWindow", + "when": "viewItem =~ /^openremotessh.explorer.host$/", + "group": "navigation@2" + }, + { + "command": "openremotessh.explorer.emptyWindowInCurrentWindow", + "when": "viewItem =~ /^openremotessh.explorer.host$/", + "group": "navigation@1" + }, + { + "command": "openremotessh.explorer.reopenFolderInNewWindow", + "when": "viewItem == openremotessh.explorer.folder", + "group": "inline@1" + }, + { + "command": "openremotessh.explorer.reopenFolderInNewWindow", + "when": "viewItem == openremotessh.explorer.folder", + "group": "navigation@2" + }, + { + "command": "openremotessh.explorer.reopenFolderInCurrentWindow", + "when": "viewItem == openremotessh.explorer.folder", + "group": "navigation@1" + }, + { + "command": "openremotessh.explorer.deleteFolderHistoryItem", + "when": "viewItem =~ /^openremotessh.explorer.folder/", + "group": "navigation@3" + }, + { + "command": "openremotessh.explorer.deleteFolderHistoryItem", + "when": "viewItem =~ /^openremotessh.explorer.folder/", + "group": "inline@2" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "gulp compile-extension:open-remote-ssh", + "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", + "watch": "gulp watch-extension:open-remote-ssh", + "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" + }, + "devDependencies": { + "@types/ssh2": "^0.5.52", + "@types/ssh2-streams": "0.1.12" + }, + "dependencies": { + "glob": "^9.3.1", + "simple-socks": "git+https://github.com/jeanp413/simple-socks#main", + "socks": "^2.5.0", + "@jeanp413/ssh-config": "^4.3.1", + "ssh2": "git+https://github.com/jeanp413/ssh2#master" + } +} diff --git a/extensions/open-remote-ssh/resources/icon.png b/extensions/open-remote-ssh/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..aa5a7b9fb1b8fc098240411765c04e58da0ccf8a GIT binary patch literal 17422 zcmXuLc|4Tw_dkB!GsD=ah_bgTSt?PY8CsMI$(D7bvSta{Vs3<@;?*jOks?`!?1~vA zZ6bU2CE1sWvCi_nyg$F+A3fx{ujO3lI@h_*d7g9JH8C;}7T78P01!TN`h+O}82Dcd z;Nyiq76MyW;SYZA)0cb!5Ztu>4*^Lpq+lcBYighm@_x#U!(VXDdgt{3D2NwiIr0E- zJpIfGJ+nY$y7xo;6U(GohWQ62)tjXmU%b^P&Z|G{J>wMd3z3|uX}%|7bWgYXt#0#j zb2Dz5UZSo2A47h?Gl1k=#=sq}^;7?QB3d(1dqdOKuL%Vzlu1cK50E zi5F)tj)bNZx37f}O#LM!ONGsgQ~QJ43)_!Mf-7EEufF+EQBhG=Rh3to7v|~dna8ba z8E0LY8|F|&IseVf5vZvh)EEXc`dHP`z>u+y_V#v;&N8<>e)cn$%jssZSiQ$%M3@*f zr-282eL*0H`8YbJNyoWrZT^}1>2d{O^Dmk}zvSGEc+^p82F)&@%fGR^ zidz_Vi_2lV8^p_1N`oDcJ@;IhoZf(b*|!28Ev6Xk@P~0BcJC4>TK?igYsn0Ds<_zG zr%&^Ez%@QVJRdC>${Iu}v+Vj8w-M6a1@IGcpJ?CR_v46KH%|91kpHMim=Jn<)>}^&tIb1iha<&uX9A`= z(;~<^RqS&i%$%w3X8r<~V+P_GF_Kc+IJr}Rn4L9Hue&x<=4LBrJ(6BtUuwX(+M2Vs3ECw18;>ynID_T2-kn9ddI=inZ{Y2B*^Io7fGTu z8-U3^3HHmQ0aY6bI&%F_u>+&}LbyymtkM<)oJ=j~#Z;$>W zi-6x1fs9u_M`>G@9G?AdF$g!x&CBBq{&r5)l}3d&fCLHI-^r<|WW_4)s*}GAjxP*# zM|siMi*_Y^37atBVR~KsX2&@2TLCC+DPH!sAcQRoWQP`g9XP_{JT9QH69a68<=$iq z1{V!Eh=5hQa3~+4GQa=_%1j3PD?^r&r>3#nnV3S1_X&VzxjiJ@qR~W2I+;V7)w6+e z$i5ALblGINpTB4&{8GLyIEVwS*IbggxpZvcx!!W$OfF}p_k(C9EnwDud3BO1zgq5? z%zpwW-+-gD5*5*5Y(G z`*L1HuKZ*u)7?D0@xDHa(U;gJF0x?3wq@?W)+~y#J$g@D45%Lx;QlfDX#OP|LtQfW z>G$;S6E_!oi>cXUtiv!dp<4wBW9!DO3SvYyj#Tqqs3`m3{9b;&1K2nb~0~i_6Z^vk;FsfS$L5 z^4zvOQ+i#QRm(%`m}FY6Dp|HEy7(w4@R4?4fZTyH`e-Sm7Mr&7-DL-fFb()Ia3U+S ztK=$CjJ}-@jJdZogj$a{l;0T9`_;&_)%D1drifA0DXMyBXOx->dF`E!0e#?dWO{zK zt9bGylgTXAkIZ^P9{wzBu_ATGap3Fj)LY))D1%0fCPgt|?Q!+$$je=#%jUVExr@(R z>v#Co5MPEzdr2(tHf{;jqCDXPDjF_J++!MZRbdkBNvLeHEve021}fsb;+V^69&XO) z3i@;#C_3%26c8}elT|~L*?K~9&i2F2`%jMjFn@_eh4cOt%2#PRfJ2|USbv~x{gb`O zM7$<=mH0HSqux!zBpV739}w;+x=D)-ow{PO@ZXcn)Ueg+AH?Ge?HwHsw$|1ao13g~ zXtZ0iAI)N|AaaAML)DL}WA7~*1V3p8U#!4*2x>?nl<>=GI#neli)+o@?3~352Y3*F z-UW-p(KzW?(4=r)>Yr0_e8vUKf%4H5_nI8gi$*H~jZe=U!d9+%ETt;>w>2_!i^40A zC@;|_VHYtBanR6#+49#w+I&esTu0-y76su0Jjy{-2j$Sp3(LQ*wJj3Ud@+7l0Ry0v zu#1Jy<_xDPOwD4Gm38=(I@CC8Mfpet8-UCEeUrc4^{ae%jQ**I; zKJ9PszY7P(r%!+ZDfFC+%9_%q%;ycD23^ZR~(o|_lbc6CN+-<*f1ieNX3LsQLw>0R{%RQ z;zBR)Co9qx|FdV&J=$R5?@ zv=LFdsizIbR!Tz}Lw|f@FQo}0Qs9NWc%W26p zj46ra?6 zkbhSuZ|Q2MC~^EoxyBv$NG$`<#^9_d(m{aGWEX~ z9|Mr&BY*Lf^9K&^I?czYA_@xTR7Dn99~3vl5>U-=3OHNo+i1Ipt-pR;-4J(;$iRbu z;OQ23lK$3Jjz*D{JC;83{TW%hUsm+42@5H*<5eUDnFv~EJwb@2tXwLUCb2PX8B1R> zjR9>DPo6vp&=)NBSSK zm$bKH2~(%6jC1)wWcyvYv+2)u$6Pno8OxU9BdrMrWN{8t4?L~3sE{qqC#Ji8BDgD#SWG4%f_ z`q(tuhk)5=D2;f43RiyatMFG2ym;Pq$b^(04KDUBWDb%4{rSoX0qD!Ry1F@N!NfqL zh}Ss)+F0G0@p8Idg2KNN0Yx{d%rgD_D^Kqe@MIkLj}yJbFk2ZB{fR1Q**`L2N9U5| z*;u&!u!W>G_~_oI(m2|OCpBH=C>s#s8mC~IYv_G*rl)+z+6B;bGkBUkRoEC&x~l;z zPUA^%>rXA@fCT$jaS>bhi<;cRE7Wb`SXfvn!A7KV^W>r5m$+f5N!cqbI@SwDRIFu%&;1)Hza$|ERP8Dj-Yxw{ zkl>beKtC#|<#^=B7|gH*ov}RYM~FR^{R)Fxa|@O~*w;`_eEmcKkB{4dSv)>BH@DKE zD?Kt!0QBXOqd9ts!(@~FG8KTK!;SqOCK^~w#&LntS#PiG}3bwOLTmKw%X z?x@F8^YW^e;VGmDZ}}c9sBm(U@GO)JFJtr0V3Wq}en%3Pbm%PBtwl-PRax-ShFdI)WQVc>)Qg*EU|_1+(=mw~^G~{?s8+%J>aFBse!9%$}&# zUbIQ^_-`Jxdgbe;>bnnE<0?BLbtPXEaXSKg$p`B{Anyf1WHmw3Fd}Pd{c?2eiV>rz zqQmCZm+UvaFb8v0Gg|(W>$B#X#-KO@>@#0!z8`x*V9H8|u=ySvE=-Y;h=YI@Q~YmN1@$wMBm?h1K_gx^c98Cy+b#E8SCVAP=FtGyDHm6K0MAl3t^_s|^sd+mJ(8gHBOo!8<|kZHIlj zF>|);7P#kDJsSXR06`;LXQT&%j!vXXr~``X11(&|PfQu)ZyWdPqup2l{F`l`Zz@$q z&w@E?w);pUkO36W380ZHNRk7cYj~g$KHP$_s{nrylB1*Jk$gD0xY40gnHZTeKja4% z?Z_qPzzwyVxxY}wADG1?@&f8nZcvvzphw2iT3RrYPIz`3>5B#k%;A_XQ*FyP0x(Cp z4&qgbIcZ+J$FHsLtFey&=#QKvDItuZx)IW+_t-f@_0JRXAR)q1lF~As=-+%EL$qbf z-^q;|A>C^u00o|URr-5#N%}i1DePAGddp6#D!gVNzwRSWBScB=r#4#9TI&D*VRd!Y zCvJOqF&>SIirQ3-WRn5W&E5{g#f7_x0v{%K{{N=MKPUvn4nk(`U`~6rK9>wo+dwQ#LIz5Gu zUsp!|HYBsP+#@p~c{rzw*Rki| z2D`X&9ZDE5F!=etbaj_TH)!!d5=*&W{Z+C6xM2K?*I%uD=L?0^FZ^!;NVdlPTHEeV z-Cz?B$lxL%@kKpp4dPrUBp1$$gKOp9B$-mr-k~W-w2w|+NjyS&rLua)89a?t%rsbE z>StPQDCyyKv;a#VD2TjB@FU5{K$dDesnw(pJ&-*ra&IlUUIY}dxo+yOhm6=I>@4%?Zw{N)>Unr={M9*Kt>eVeXGX?lq|=bTjKHqfR^|sVwZovb?Dv;5@)yv z0UvNVs=K8rScKH7=CKv}6LY=%b>A^QTp`jN3)jvqklqL*}MeL=au-rEPZ7lyv{ z3A<>g@y8qbg0O2CrE5%=Z>)lc&VfuwJ~)H%)8c$Oj-b*20jGz5zC2ppH1zG8VZick zIRW7A?ha=Yf#Z@3Q~rxP#7R@(#9&Y+KUjoP+xf!(*{#sfQ0`p3ya0G*Mu= z;J0x=4_G-kXdUJ3o`M3Me`vDq-`p7#`n0P!I<#RSDlXL!{NdK}4!Sp9gR%lhJwxlCwu^GNkAsc%b}*u>s!L2Oj#g=VAgw` zobTzeC6CeM;i5)bQm@vJFfhmcn!h9(gkSd==vVF?9kpW{w;}2wLh9>BzRQVaH9%_) znHNDBKD>~``r#~;4a8c|OMtRI{)$c>c1DOx#~g&~p8H&OljAK)RR4(k-w`}wlnLh- zzJK2=99^~YHyQT$>|7aWF+H1;mzQLZ@tf_d{J*x)ysrL&DxQQb@YjE@y1eATVd$^Z zugG{}r`-4wj>6_I_n0z7X9&eh-xFK^HQtT9TWW|;0f3f*K3?@{*oLzFv|pws-UNpc z02;j47Oz)S{QThzhPsU*&uyA&UBl*+`)?m~{wan_a-4lYx}&xcxIBf(+G0OM8s#5A zFM}*9{W)@he03|FufY$(ksN6~`UB>f3fZUe7+#=ZBY{f^TOAJ*>--SlMchy^0TOvY zG(ae_?{bj{V(5=}1hUEPl^DgbdVaJK|K~oO=xy(4Dp{^CWVSL)ftVr=D&XZ#%8JT$ zijvM9zgt%B}mb$u8k3mf#@*L6~|wGf&&ojfEW3#^c;kuqvDUP-tnX- z>Ud&sfF7ld$cHUET3x!te>yG>1t8C_`hEpcVQ*!?JNox8hP44Mea5U1X(VuOp#;c+ zbFMx<`=`1~)uw(IW^xNuX}>E&0=VZ1o7qWWAGhS*&UPH1XqK$(=%a?6SH4o&@n|{8 zkleTKv@LIkXRY~8pSm_<{)g!!tJQ1ns8_Pa&8|WF+}m<$e*YnX5F7xV5S5MIof7!U zVUDTk#xr|{tNV&8C!6_bkVq-Dr%Iwekf@>*i{*7kfqOnmGbVx{!Va)deDg}do4v~9 zglOYBV_m`9MLJN>vL3OlD?AhbUY0ZOl1$Va`1VBE4=?a~Wy4wMIqRgEVqJ7{6a6>Y zwZw9itZ$Ex-wKBG%#4km$-SCCa{{iUkz%pdsJmpV3_H|15`D21cMNQJlC~3-nTZH! zI^E{xfc1SdH6HToMeSL&%2qXw0OH5fbr%52g`zG3#16@n_!`8dWTyR~i!Ym^_GhYlYeBqw^Sg$P9W0mFQ<(>e=Tv(obL#ub-{CkEA|NFtc3!$)i z>9H0Dds%J{>0#z#P3@>$PwhS8{M4Ona$(~;=9W`ny4E|D>gG-qopH2kddAeOC;^-} zgZ&t#@Gf?eJ#|PRF0iD}`63WSJfNj%RK2>(u8rfyXL8o>kzvDbIr^z!JCi}LW?ha0neAWZ zJ8DE5KEm?3>a^R8jBlGv-)y*QC3~^5!{YQs`=i;LWMw2?++#ihOu}CT6#=N@UXQJd z%0V=4d#=qOe1k2Jvb#Xm#cQB3JSP$pO-`aq>0c|BR7PmVTfny z9O^8i>_sbo4SVYfaLU957D}M1oZYB|Gc%t09@tgN<2Z@vpe2*h>@V#W5O|X-nnrsx}{69ow!?-@LCBz~jyv4=$16J31OE5xW218Q!klqeIk=9TIF)i?v5hsprY6m08;6F&=woy00?FZGQ4bQUmW)(pv$Hxq(8a0-ZGQHwTO6Aus?7mS;Z9 z21A=j$fA=MbQ#3|YZ!l-fpD5H!jPbRj$+A{>*jyDs==?$0sZ?XOyu<0mg) zI_N*|#biVRQjjE?5xxb7OHyG9m|hRFQ%%<+SaVt~Xb}rHW;tWUcXV4Hl##==WgnB8 z0Sgg|B?$Vrx$pY7A!^&PY)&wIEe2!ccU`X?Z9Z@4*pX*fZ8`;PvyX*|fz6vY=kXl( zz=DR?XRj3nB*zLovEaPePL0=Ant1(M=1YqvJ?(gmez%qU%Un<7F&?{dX^b#qP3 z6U`{3v!YWMVs@JIHCYg6jF03zVq9KHGvXdc6lR23d!!r%fy?G%=Qhq#h6QbLE$HD3 zdG^c9;9!Rd)kj*xV6MrPc3&109sx!e(2zDeBED9*_e76$>ZxyHlr4`mJ$S5A!CN`3 z?YPHsoxOVNV&O~T#n&$Ei>#LH2i6&U$n{QveES^%+Ap(3*@4UW_T5M#7JReRwQt{4 zN086bC%(&jw0m;vmeLgiFT^SpM-l-_sY_w|>Ij4^eY6CV^zn&D&raIWTNt8tGAqMg zy=F5o(S&G;f-$31Yx-TG0Fsi4E#hhVBN(A$48Ckf=>K+rNR(KYoYsrVC<9(L1=mEE?mFCJ$> zT>JCL*fy2c^vNk)7xd=w8kfN z;@O}ncJ`caw?pL(WzMT<*?1K>s!DdnrH-0hx0Nr@RaF#QLO{?zZR?YhH2Wi)xv};8 zh<6X2oF-4Qxq2%7trj-GW=6KPdhj0>Z2DK`WS32rkZRpNI;1uM0&i8F?dHut#-&wN zRb3UH^_UlR;ERTS!wMsC!&Rn3YEK-wZrInpOP%81nUb8`{hs;$DzLJ%s}vJ@qEpVV z@t?(x_F4#2O;t4q@jahIDH@%2c!j`xBZ z`Hdv`0$)+sDYRbYX`uzeNZ&Eo6nTpOLk+G7WSI!T6AYbGo(o8cn!*>;~qg8~;3~}za`_ftwJFh#Bt5~-8 zyZih)X3-$01#J1EiyR)!?-q2?j*nYrBgbK1Wjr@)nXuI3 z%Z{H^eJz({&#GJ1K&_|C+r;DX*X!z5>xVgg1<(`pAv#KfIcK*1xGtLjb;iypR8yQA z%XCG1j1rId?VNew%c35*3Be|jFRp-|%CsNPwJD8w)(}g6b6cz_V@R#kt6${ljc#7= zz^3*ST1!UndDJhsJ8oUQ;6Ay~C45M-vAA&Gax#9c1 z;g?WdKotMF8s{?m(8Jo2_f_>drQ0vBam^B&J|P1iW~+`$+#BI2;3<0yJ-LmhkR5Ux zdSd?`F+6H?T^8KvXM7-@*unJpPm`L@xTrM%}ju~+#fyYeGU zJvtc$Cb#@2!%>xau-&MK^USpbeDae<4RF|FIo%H})Mi$>xrN>vG0g>XxO0^}6l$(a zHYKC3=oz7W{M?UA$jz2C*SQYuv& zxJWCUo9nE0RTtvxe1FSlVFH-(A?YJ>n0+)0Q}**iE3@ikZ#R>;%md#yqbb63A4vBc ziFBh!7*q^%>iZE~&xZgtD?^6!P3@n@T9~YZFoNQQKVOPS@#3a59C=C5M0F{T29xzC zL%2bQSfg{c$$bYKbq==d>pO=Kbt-@m7Nfb(K`PhM-$oIVICNF-P1fYYch_zC4kjG| zKN=dI6BJEq(OU(Ws}Nl9(l@+~{^CPQWIQ=73)~Nmrw-!LH*em!w+CRx;vU!Iu!-V| zBn^F37=pPqLeAWnX+CDtu}?SDUw;>eF+NPmA`EXIJp5as(w%EY!F_)k4L)pVA8_+& zA-C3zA7KC1qUMr-sJqYd)*dQJHcCw0`kPq7aL2dTFJ{=(tIM<1T+6q2XK?v)yyDTw zuU+kGpZCO1ywT^AQZeVtpV=JbaXnC$o$a(tHFd@lf0QB%4X2U?u3z}~O^NB-Sql5* z-t9)(5&HO0YF9K@j>V1I8oJaQ(8a5sYCUvWfg*Q4l<)gtc-%oTAg21rMWbx4eNJex z>U(!#5a~#qPe#h6_^vxv#(H)O(N96X>l{Z%I?9=6wo=>3>VeDly*6Nw!(N-kJFP~% z76dyZA`R(t4KuiY~duWfpzgaU21GyG2Mj;#A zm9gYzPRMYwTjjtfCuvj_bk+Qe>No(#>Iz;b81YG+=jnRibSdm*4-6(-`BW@w@WL(muV;KN-I7P*3Bn> zhUDZnH-?p4eH(e&fE)ds2NZObN?Eu`qGG7er7GgYWaACPipL_vi*e+uNn!&RpfV2* zh!a7FC%7lnuWSVikLd&6`%SV6!EJD_Hdvkw9o6bs0k@g{wUW;)oC2rwe8F{P&XZhy)S2_;!evayS0@I6 zxe3hacf*?g3OJJT)4}#&!`|TyS&!V@8B~wWn2&L^bu)dOj+h8xoY5@Z^&5(EhAeC?$Ac?sZcP%>ATNH zIO>WnpG&3iTuUlP>?N+I2&AGyrO%C(P(@Ip%)ZRm8?FKt^Th9z{md9!7vx?JZk$xReW=m#aD66vE_lt zSEF0irHeV)A*u`h*O_y2p}BiJtR==L$}D5uefDrxes#QOPFmYJxbNkhw`Iq<0~5)C zp}II)9QjK?#QsW`3F+2LtJNV9lpO(Co!2hLsT)+%Bvhn(^)`dG(KXg zzpOHz=;Q``TrjY;Ie?)2qlj)ZpG?_ zJ>`IoGg4H4Vom0}V`zE`Qw9yxEsY#XRr4=4lyVD&x1jO?o9V@#w`VVLD-{u{D*E`= z5i#{2t2&h>U>}v|soT-ojdcVsIR~si4%CahUwO$*-@y+iFz^!6WMy1LGZ#J}fKg`I zA1qBI^M22hfxUxu7ZW+D{uZBo<{C$Sa{uGbBz%FXS6Rz5*4`%nd@-nFyW~Vqna_5s zLV-WxTvpIjR^#f*4LR0X$nh%(5#kxP6(HD=qzujv`1pNWo#BsD{s0*|Utt)~zDRRo z7+0*$a@UUQ8$6IH7c0f6;_&Co6vgHeU(;@_+{m|ZmPCgfc;jod-PatDI`=uXii8_X zwKG`s6PWY#ex*tr*TP}iyMbILGBJTMVSOJcUQHdD|EsIE*^*0*`t%SF{E?3jcqZy1 z!ikT6uG)v@zS2>hdd>{_59am8w{dfxWsK8)!HKn>40ZOO`iez+Xk_jQZ|%)*!-+Gz z#INuz&N0S*+1GtToO|PO{?csEYR4Ob=gZmLD;t208h&F5#^k_cy5B4PF^0lBSNWbd zij~>vF5{i&db9x7Tkv?K|+~?_qM7@9(<25+C2XB05p>=^KALs-{Aw! z8&2EnsuVtmi&Nsr_y!Li$`!n*oKSn7 zoo{(YI&De2L08uQHh>E@Mzt5E9_JB{Pv%$7if3Pc48xE*o0Z@QGr4Y1v9)t@JhY{+ zW&c?|%5wL=t?!a=F01M7UN)O>B1>PD09V4oV2JoxI58BN`$T;F4a&jKRD!&Y(vNd% z6pE`psOx_^YSLR2!u@%XtN5&%=J=Zj2pa^7;Ixg&nBrfWs6YPuf+kE38m$vHZ+pqO zaMieJwb5t4g31zlK(g2dVk=t)6Pk4G)(OLIQ8P$vuBUhyJjR@ zheSzZPTC+c!Y5pHBC?$&i3^p7zpIdI711Ft{<3LDB+ip8{2wwcxbFK`Y8{H6v zSx?nx_4M@I@O~B2WSn|TcECVwn@--!h2X)h&TaqJV+Jq>jdWhww=wsC1mn?eS517_?iADf&ce1)MnvrF z{yRscLmPr`Rl1G5ku;^MKF1_zBW|N1I?3SCuce%&k}S;N4jud^{Ch99<@-L&+qd`M zS5?idN`BJ;hOemR@l;C*$Jtyk-Rt(`YrC!eM)7ve&7V7ZDY!Mx$s&&ONgTLjZ5`Y{ zpG3M)F%kwI*GziM!(2N%JSIvkK{7NqNYX-rb7qlqacV6_h`QEmxF4P+m49<6^6TlT zJ@7HP0rR>B?u#YFh}Suov36f#yH9@cRo^e(x=Iz70Ie2`V-gg}R)`_{aNE0+Z`SGL1&lBY)Xe!|cu&wx z#Po%LRK|>quQu~yzUi}_=8V)LW-4jSxX)ZgAPT0aoo2<6m8*P1n$Odr?Vnq1mD z#b)<&Lr)LDR2D8*I7+z+t5C7CDMXHVcnjJSq`N2oRS+tZl9o2?Ho@DJ)eR#>*33~c z`tEWEeQM?1l08OQe{b`;oB#=HOS7qbKMk-);J9)3*0qzS#=FiP5x4r4YG+WRAw35% zI$oWe3bQ@G=6bfbUXWedlXEw37Yyzb&nmph+vKvr00>TB6gtl8*){K%Il7PWoO zWfw(xL|T^q64)&ShDSWYvt791jZQc1*N*tmya#JRF;Ct431Ngoe}A{uH3FftheRn} zN}=1?Yc%c$?G4ByCtmu~s$>2sU6Yd+xhW7#Tk8Ivwe(oonla&?lRJ;B${oG&=k#(t znN^>Y;`gqxmd2T+MRQq=+@8gyiN-lK?mx9G?alW)m*m^u0tNZdgEe#X&th}5 zh;(`Ga{VBcyGq?%LETDF0G-YK)Y$hOM9Nr{ZmF0;tsc=%Kv+%lxy6s9sO1>)atsH3 z&AZe0)eu*n%aC_s_mMfHWTOKC5|&(A+!|-MeyxTo6uWIJWEeV{z)T4~ScrI~RXOH( z00%;>nZ0E`wZ#MGiUR}Hk-sJ6Bp%Y(-L%mBt)5+Xf|p z$v2#RcOUFtyv*?WGot33mImFo3J9}XKltFa>Kc2fjrsOX^WKxS*7S1K z`UDfK#Q^Iq9TpR*1}ZD!%*1)CRwuHitq1pEWhy z!};~$jl=r#cyL#7z)T3C-p0W+ZAGwbx?v55WghB&D=iXJKi92fuGCa7z+*v<-@CZF zX1a9Zq=PccR8796|Gx(@52ST2Zst}u_1mNkT5kZC5&Irk8T4*0e4UaI^9!iFFm9qb zMchynkj5RjvB6HW06a2+^ zOURysZqVeYHle)p*4jbj19BPLGR5frT@9LFcM={}ENcagd8M~QXUYb_4_Tn^ydO@| zcl}<&bm`Z(21Ym>%<82+;g;F#Sk*qwl)BuzphX~X{cK`sMSH<%xL+uDYYBWW!N3J; z24f)Q+OOkjyMA9b{P<6_K=otb^4wry+DU6k@CTa7yDuk?zD(d#1c8rBFbn9jjH!^) zCl+hLp)EcwK5tYxX*`>_$*xyH!vB>_H2h!J=mEcBn?0ky8Tn^o*gO0gUf(})uUW(T zI#2wI`0J@7>%BTYa?o~Um;O4clFR9p`qYfL*Q8WN&X8gJRiVj&GIFjY5ak?Uh6>&Zkb}n zRUCoQ&Ku2$@k?g>>;qL56+1x%s1a5c=-*zKUPG>@{{}qapR&KS|AI!xlNOy5hN%Z7 zxUHqfK7%=pIxZfE3Y$8J@3;*}A=YQAmWSdz1f^Og6D(LQeV3EIt2^)GQ0s=sdj}8< zeV6Gm{jviMLDrP}t^7?TK#yYY#>noa3WjF%KK*g^=Jw6pQYQwPFrbJ-H<H#|&@hJC~VPqqaq6KHG#yvhyZ;(Y1lueke@x~8kVw@lbX=F&dXZgPa+Akw>QDpKJ!JBu z*N(!tBqfg1HgHY>CjENZHt?v+@SQV=bTyZ1vU!v>mOYR?XJdQd>971$6V`4bNHl@; zNr~VlF7Sl!^4RqDOZI?eRR6zgzwRxrMGX-Y*DEvxkRLpN*=%3J%O}AHUGQh23MS!} z1syij4&U;lC&&gBErgNcDrpl|CY;dJND)0736Ac)*)?n-)wDKIzXx8;-91&IYtpT!tJ0H zmSXS;JDmkYy8|98^Q6=ySo5;+I6Yhn(@J&_dJad@_uX(`sjnexRaFN5`SPD2au5!K zWm(7W%ilb{OJj#046c;mz&(}PJ>>DM<@zVw|Eru?4_@2Z*tER=Abnu}L|XW8c5>e_a&7*S>UMnRFYI*kXfo~625**}3 z7tX+9F)JbD`+l6u;e6=>btS+=q(sIY8UM8>1pJP{O*-h zKc_eF=j$U6LBQ{t%7vwO--9Y>Ufxw)yT!G6eelI(j|~P^n+;G=r_kEKhYlHh;CXZH zZ5STzi|<7(^$|V{TKU3@(JiFH{kQ=fu>AIv_$EnIfe$eExX`KbPsv+gQJ#|PS)v&W zrX}l(foo>ggYu+rG%*|+@h8lu=@bqYoJ4I7lN+>=!Vnz|%MQD2j{xc^rh??wIJ<8h zWu2M(a45$0EXm)Wd14PXqE>1%PR`tW@L3@=oI2fwR-eQGll=A9R9i}HD!K+vJln{S z*9lbrG_=L=pwvQ(9VZhLdjcrge(eS2VeH^C=;MzugAtcFQ`NaGJ2!I$;aOAnk6CJB zh{qrI2#e)OoodM{rar04n^q~C^ZiKGgB6gb!CFLdoM+$1uZv)7`W%~$w_<9E!n#Jo zSzfbN;ihq<1aI=Hxtk@CyL$BHPxt-w7s4*$aK=Mv(k59Y_|`2HW%R|NtHln3d;70# z2O73hyfSwb2U<*ECFiR&y~_RYv&Pk8!fju+3K6*6ZT8&IV{}NFvI(~yPC>OKLWLK` zr5rX{ti6Ky1nh#jR6Z9S3^7i_)rOQ{? zLx|t=FHBYu(EcGv(L8^+s#Lg(t#T5$6jYa>6-aLrQ2=TC0UW`jw-&2=mxWbUi`Mjj z%ZCh4L66rzi4uH=E$%e97(3M2E3;wN);5hTEo?CiSmo6lSlZ9GFD5#hmyfn?@kp)L z6l1##a@p_3v|JOsl)Faby=I$dV#L5)!mdXAJyU<))}552O&;deUN_vyx7~eGxz_~< z(HlU1mL@L(GZ(@s!NYePe?wKdWK!)_yO*4}xH39A+OO?5)>Mylq5xGXKE!7hjYVW5 zSgui2_FBnimf@j+rF41rSm@%)egOn@H{YVO*OnZv|GOK0#>|m2A?979Abfrz`-}S% zSj<)mM+ij8KT-fB4_tUd)>#uCOr@m)X_8`JTW2phR}|z=+af^7&;#^Oe3XVuWXO`1a3R$gt?k<%wDgB>J%p_&e4m$@#DhNRD> z8^N0Gchi}?AC=aFBeQxPlwfMU$?wh!O|$)~Ivy63?3z4G-sNK~-?k8e59Ri^LbwI> za%+h-@f9%3N?%W*D&&o@mLSHWtX%XdA5fQwq=zrh zh$a71tWeV*-pyySRf4_j9}<2h?9V+|WnXX-HEkL)IKu~&4E?9Q6@(NviHAOp1?c4s zya@s)<a$I@M4O0$k`JP~pyLEbqVq?={mDqb9}C;#~(m-GOj?OXite(wfG z;a^Qa+he2bPjcdO{{1?I14T&>q&Q1t?$V9SmP;bIh|k#qVLjhzVoU$jOy+)F!vWGA zOf}5mlZ-+~s#$b|SKzx@@xpL-s?{Jiej5PYO+tFNIz)LG)6RbU|6aF+vU=VQI5>&Q zcgn%^_=vxJLnYOXm(zaBla~vLne+zI)qZK!MfgYr#<%IJZ@~bQP5uHyeK#y+BlHfB zHLE1{eb%SJ+SyTf^qvAxy?F zp+4TrbPSQl0fjWTF%!fwER3_eATmEdOk7-O!NTM8+t`p>RP&FjZ7bAw^m>9*e2H$t zW&{>G54;;b$rHI(=F{b_Hfr7F)lHT1eiD1*q$7@N-E(y@hcc*JDbKpa?YL~UG9T@5 mF^wi2`NPg)mdN1{9&P!5d$wS`Otvlq5O})!xvX { + const [type, dest] = authority.split('+'); + if (type !== REMOTE_SSH_AUTHORITY) { + throw new Error(`Invalid authority type for SSH resolver: ${type}`); + } + + this.logger.info(`Resolving ssh remote authority '${authority}' (attemp #${context.resolveAttempt})`); + + const sshDest = SSHDestination.parseEncoded(dest); + + // It looks like default values are not loaded yet when resolving a remote, + // so let's hardcode the default values here + const remoteSSHconfig = vscode.workspace.getConfiguration('remote.SSH'); + const enableDynamicForwarding = remoteSSHconfig.get('enableDynamicForwarding', true)!; + const enableAgentForwarding = remoteSSHconfig.get('enableAgentForwarding', true)!; + const serverDownloadUrlTemplate = remoteSSHconfig.get('serverDownloadUrlTemplate'); + const defaultExtensions = remoteSSHconfig.get('defaultExtensions', []); + const remotePlatformMap = remoteSSHconfig.get>('remotePlatform', {}); + const remoteServerListenOnSocket = remoteSSHconfig.get('remoteServerListenOnSocket', false)!; + const connectTimeout = remoteSSHconfig.get('connectTimeout', 60)!; + + return vscode.window.withProgress({ + title: `Setting up SSH Host ${sshDest.hostname}`, + location: vscode.ProgressLocation.Notification, + cancellable: false + }, async () => { + try { + const sshconfig = await SSHConfiguration.loadFromFS(); + const sshHostConfig = sshconfig.getHostConfiguration(sshDest.hostname); + const sshHostName = sshHostConfig['HostName'] ? sshHostConfig['HostName'].replace('%h', sshDest.hostname) : sshDest.hostname; + const sshUser = sshHostConfig['User'] || sshDest.user || os.userInfo().username || ''; // https://github.com/openssh/openssh-portable/blob/5ec5504f1d328d5bfa64280cd617c3efec4f78f3/sshconnect.c#L1561-L1562 + const sshPort = sshHostConfig['Port'] ? parseInt(sshHostConfig['Port'], 10) : (sshDest.port || 22); + + this.sshAgentSock = sshHostConfig['IdentityAgent'] || process.env['SSH_AUTH_SOCK'] || (isWindows ? '\\\\.\\pipe\\openssh-ssh-agent' : undefined); + this.sshAgentSock = this.sshAgentSock ? untildify(this.sshAgentSock) : undefined; + const agentForward = enableAgentForwarding && (sshHostConfig['ForwardAgent'] || 'no').toLowerCase() === 'yes'; + const agent = agentForward && this.sshAgentSock ? new ssh2.OpenSSHAgent(this.sshAgentSock) : undefined; + + const preferredAuthentications = sshHostConfig['PreferredAuthentications'] ? sshHostConfig['PreferredAuthentications'].split(',').map(s => s.trim()) : ['publickey', 'password', 'keyboard-interactive']; + + const identityFiles: string[] = (sshHostConfig['IdentityFile'] as unknown as string[]) || []; + const identitiesOnly = (sshHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes'; + const identityKeys = await gatherIdentityFiles(identityFiles, this.sshAgentSock, identitiesOnly, this.logger); + + // Create proxy jump connections if any + let proxyStream: ssh2.ClientChannel | stream.Duplex | undefined; + if (sshHostConfig['ProxyJump']) { + const proxyJumps = sshHostConfig['ProxyJump'].split(',').filter(i => !!i.trim()) + .map(i => { + const proxy = SSHDestination.parse(i); + const proxyHostConfig = sshconfig.getHostConfiguration(proxy.hostname); + return [proxy, proxyHostConfig] as [SSHDestination, Record]; + }); + for (let i = 0; i < proxyJumps.length; i++) { + const [proxy, proxyHostConfig] = proxyJumps[i]; + const proxyHostName = proxyHostConfig['HostName'] || proxy.hostname; + const proxyUser = proxyHostConfig['User'] || proxy.user || sshUser; + const proxyPort = proxyHostConfig['Port'] ? parseInt(proxyHostConfig['Port'], 10) : (proxy.port || sshPort); + + const proxyAgentForward = enableAgentForwarding && (proxyHostConfig['ForwardAgent'] || 'no').toLowerCase() === 'yes'; + const proxyAgent = proxyAgentForward && this.sshAgentSock ? new ssh2.OpenSSHAgent(this.sshAgentSock) : undefined; + + const proxyIdentityFiles: string[] = (proxyHostConfig['IdentityFile'] as unknown as string[]) || []; + const proxyIdentitiesOnly = (proxyHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes'; + const proxyIdentityKeys = await gatherIdentityFiles(proxyIdentityFiles, this.sshAgentSock, proxyIdentitiesOnly, this.logger); + + const proxyAuthHandler = this.getSSHAuthHandler(proxyUser, proxyHostName, proxyIdentityKeys, preferredAuthentications); + const proxyConnection = new SSHConnection({ + host: !proxyStream ? proxyHostName : undefined, + port: !proxyStream ? proxyPort : undefined, + sock: proxyStream, + username: proxyUser, + readyTimeout: connectTimeout * 1000, + strictVendor: false, + agentForward: proxyAgentForward, + agent: proxyAgent, + authHandler: (arg0, arg1, arg2) => (proxyAuthHandler(arg0, arg1, arg2), undefined) + }); + this.proxyConnections.push(proxyConnection); + + const nextProxyJump = i < proxyJumps.length - 1 ? proxyJumps[i + 1] : undefined; + const destIP = nextProxyJump ? (nextProxyJump[1]['HostName'] || nextProxyJump[0].hostname) : sshHostName; + const destPort = nextProxyJump ? ((nextProxyJump[1]['Port'] && parseInt(nextProxyJump[1]['Port'], 10)) || nextProxyJump[0].port || 22) : sshPort; + proxyStream = await proxyConnection.forwardOut('127.0.0.1', 0, destIP, destPort); + } + } else if (sshHostConfig['ProxyCommand']) { + let proxyArgs = (sshHostConfig['ProxyCommand'] as unknown as string[]) + .map((arg) => arg.replace('%h', sshHostName).replace('%n', sshDest.hostname).replace('%p', sshPort.toString()).replace('%r', sshUser)); + let proxyCommand = proxyArgs.shift()!; + + let options = {}; + if (isWindows && /\.(bat|cmd)$/.test(proxyCommand)) { + proxyCommand = `"${proxyCommand}"`; + proxyArgs = proxyArgs.map((arg) => arg.includes(' ') ? `"${arg}"` : arg); + options = { shell: true, windowsHide: true, windowsVerbatimArguments: true }; + } + + this.logger.trace(`Spawning ProxyCommand: ${proxyCommand} ${proxyArgs.join(' ')}`); + + const child = cp.spawn(proxyCommand, proxyArgs, options); + proxyStream = stream.Duplex.from({ readable: child.stdout, writable: child.stdin }); + this.proxyCommandProcess = child; + } + + // Create final shh connection + const sshAuthHandler = this.getSSHAuthHandler(sshUser, sshHostName, identityKeys, preferredAuthentications); + + this.sshConnection = new SSHConnection({ + host: !proxyStream ? sshHostName : undefined, + port: !proxyStream ? sshPort : undefined, + sock: proxyStream, + username: sshUser, + readyTimeout: connectTimeout * 1000, + strictVendor: false, + agentForward, + agent, + authHandler: (arg0, arg1, arg2) => (sshAuthHandler(arg0, arg1, arg2), undefined), + }); + await this.sshConnection.connect(); + + const envVariables: Record = {}; + if (agentForward) { + envVariables['SSH_AUTH_SOCK'] = null; + } + + const installResult = await installCodeServer(this.sshConnection, serverDownloadUrlTemplate, defaultExtensions, Object.keys(envVariables), remotePlatformMap[sshDest.hostname], remoteServerListenOnSocket, this.logger); + + for (const key of Object.keys(envVariables)) { + if (installResult[key] !== undefined) { + envVariables[key] = installResult[key]; + } + } + + // Update terminal env variables + this.context.environmentVariableCollection.persistent = false; + for (const [key, value] of Object.entries(envVariables)) { + if (value) { + this.context.environmentVariableCollection.replace(key, value); + } + } + + if (enableDynamicForwarding) { + const socksPort = await findRandomPort(); + this.socksTunnel = await this.sshConnection!.addTunnel({ + name: `ssh_tunnel_socks_${socksPort}`, + localPort: socksPort, + socks: true + }); + } + + const tunnelConfig = await this.openTunnel(0, installResult.listeningOn); + this.tunnels.push(tunnelConfig); + + // Enable ports view + vscode.commands.executeCommand('setContext', 'forwardedPortsViewEnabled', true); + + this.labelFormatterDisposable?.dispose(); + this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({ + scheme: 'vscode-remote', + authority: `${REMOTE_SSH_AUTHORITY}+*`, + formatting: { + label: '${path}', + separator: '/', + tildify: true, + workspaceSuffix: `SSH: ${sshDest.hostname}` + (sshDest.port && sshDest.port !== 22 ? `:${sshDest.port}` : '') + } + }); + + const resolvedResult: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', tunnelConfig.localPort, installResult.connectionToken); + resolvedResult.extensionHostEnv = envVariables; + return resolvedResult; + } catch (e: unknown) { + this.logger.error(`Error resolving authority`, e); + + // Initial connection + if (context.resolveAttempt === 1) { + this.logger.show(); + + const closeRemote = 'Close Remote'; + const retry = 'Retry'; + const result = await vscode.window.showErrorMessage(`Could not establish connection to "${sshDest.hostname}"`, { modal: true }, closeRemote, retry); + if (result === closeRemote) { + await vscode.commands.executeCommand('workbench.action.remote.close'); + } else if (result === retry) { + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + } + + if (e instanceof ServerInstallError || !(e instanceof Error)) { + throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e)); + } else { + throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message); + } + } + }); + } + + private async openTunnel(localPort: number, remotePortOrSocketPath: number | string) { + localPort = localPort > 0 ? localPort : await findRandomPort(); + + const disposables: vscode.Disposable[] = []; + const remotePort = typeof remotePortOrSocketPath === 'number' ? remotePortOrSocketPath : undefined; + const remoteSocketPath = typeof remotePortOrSocketPath === 'string' ? remotePortOrSocketPath : undefined; + if (this.socksTunnel && remotePort) { + const forwardingServer = await new Promise((resolve, reject) => { + this.logger.trace(`Creating forwarding server ${localPort}(local) => ${this.socksTunnel!.localPort!}(socks) => ${remotePort}(remote)`); + const socksOptions: SocksClientOptions = { + proxy: { + host: '127.0.0.1', + port: this.socksTunnel!.localPort!, + type: 5 + }, + command: 'connect', + destination: { + host: '127.0.0.1', + port: remotePort + } + }; + const server: net.Server = net.createServer() + .on('error', reject) + .on('connection', async (socket: net.Socket) => { + try { + const socksConn = await SocksClient.createConnection(socksOptions); + socket.pipe(socksConn.socket); + socksConn.socket.pipe(socket); + } catch (error) { + this.logger.error(`Error while creating SOCKS connection`, error); + } + }) + .on('listening', () => resolve(server)) + .listen(localPort); + }); + disposables.push({ + dispose: () => forwardingServer.close(() => { + this.logger.trace(`SOCKS forwading server closed`); + }), + }); + } else { + this.logger.trace(`Opening tunnel ${localPort}(local) => ${remotePortOrSocketPath}(remote)`); + const tunnelConfig = await this.sshConnection!.addTunnel({ + name: `ssh_tunnel_${localPort}_${remotePortOrSocketPath}`, + remoteAddr: '127.0.0.1', + remotePort, + remoteSocketPath, + localPort + }); + disposables.push({ + dispose: () => { + this.sshConnection?.closeTunnel(tunnelConfig.name); + this.logger.trace(`Tunnel ${tunnelConfig.name} closed`); + } + }); + } + + return new TunnelInfo(localPort, remotePortOrSocketPath, disposables); + } + + private getSSHAuthHandler(sshUser: string, sshHostName: string, identityKeys: SSHKey[], preferredAuthentications: string[]) { + let passwordRetryCount = PASSWORD_RETRY_COUNT; + let keyboardRetryCount = PASSWORD_RETRY_COUNT; + identityKeys = identityKeys.slice(); + return async (methodsLeft: string[] | null, _partialSuccess: boolean | null, callback: (nextAuth: ssh2.AuthHandlerResult) => void) => { + if (methodsLeft === null) { + this.logger.info(`Trying no-auth authentication`); + + return callback({ + type: 'none', + username: sshUser, + }); + } + if (methodsLeft.includes('publickey') && identityKeys.length && preferredAuthentications.includes('publickey')) { + const identityKey = identityKeys.shift()!; + + this.logger.info(`Trying publickey authentication: ${identityKey.filename} ${identityKey.parsedKey.type} SHA256:${identityKey.fingerprint}`); + + if (identityKey.agentSupport) { + return callback({ + type: 'agent', + username: sshUser, + agent: new class extends ssh2.OpenSSHAgent { + // Only return the current key + override getIdentities(callback: (err: Error | undefined, publicKeys?: ParsedKey[]) => void): void { + callback(undefined, [identityKey.parsedKey]); + } + }(this.sshAgentSock!) + }); + } + if (identityKey.isPrivate) { + return callback({ + type: 'publickey', + username: sshUser, + key: identityKey.parsedKey + }); + } + if (!await fileExists(identityKey.filename)) { + // Try next identity file + return callback(null as any); + } + + const keyBuffer = await fs.promises.readFile(identityKey.filename); + let result = ssh2.utils.parseKey(keyBuffer); // First try without passphrase + if (result instanceof Error && result.message === 'Encrypted private OpenSSH key detected, but no passphrase given') { + let passphraseRetryCount = PASSPHRASE_RETRY_COUNT; + while (result instanceof Error && passphraseRetryCount > 0) { + const passphrase = await vscode.window.showInputBox({ + title: `Enter passphrase for ${identityKey.filename}`, + password: true, + ignoreFocusOut: true + }); + if (!passphrase) { + break; + } + result = ssh2.utils.parseKey(keyBuffer, passphrase); + passphraseRetryCount--; + } + } + if (!result || result instanceof Error) { + // Try next identity file + return callback(null as any); + } + + const key = Array.isArray(result) ? result[0] : result; + return callback({ + type: 'publickey', + username: sshUser, + key + }); + } + if (methodsLeft.includes('password') && passwordRetryCount > 0 && preferredAuthentications.includes('password')) { + if (passwordRetryCount === PASSWORD_RETRY_COUNT) { + this.logger.info(`Trying password authentication`); + } + + const password = await vscode.window.showInputBox({ + title: `Enter password for ${sshUser}@${sshHostName}`, + password: true, + ignoreFocusOut: true + }); + passwordRetryCount--; + + return callback(password + ? { + type: 'password', + username: sshUser, + password + } + : false); + } + if (methodsLeft.includes('keyboard-interactive') && keyboardRetryCount > 0 && preferredAuthentications.includes('keyboard-interactive')) { + if (keyboardRetryCount === PASSWORD_RETRY_COUNT) { + this.logger.info(`Trying keyboard-interactive authentication`); + } + + return callback({ + type: 'keyboard-interactive', + username: sshUser, + prompt: async (_name, _instructions, _instructionsLang, prompts, finish) => { + const responses: string[] = []; + for (const prompt of prompts) { + const response = await vscode.window.showInputBox({ + title: `(${sshUser}@${sshHostName}) ${prompt.prompt}`, + password: !prompt.echo, + ignoreFocusOut: true + }); + if (response === undefined) { + keyboardRetryCount = 0; + break; + } + responses.push(response); + } + keyboardRetryCount--; + finish(responses); + } + }); + } + + callback(false); + }; + } + + dispose() { + disposeAll(this.tunnels); + // If there's proxy connections then just close the parent connection + if (this.proxyConnections.length) { + this.proxyConnections[0].close(); + } else { + this.sshConnection?.close(); + } + this.proxyCommandProcess?.kill(); + this.labelFormatterDisposable?.dispose(); + } +} diff --git a/extensions/open-remote-ssh/src/commands.ts b/extensions/open-remote-ssh/src/commands.ts new file mode 100644 index 00000000..ccc9c7ff --- /dev/null +++ b/extensions/open-remote-ssh/src/commands.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import { getRemoteAuthority } from './authResolver'; +import { getSSHConfigPath } from './ssh/sshConfig'; +import { exists as fileExists } from './common/files'; +import SSHDestination from './ssh/sshDestination'; + +export async function promptOpenRemoteSSHWindow(reuseWindow: boolean) { + const host = await vscode.window.showInputBox({ + title: 'Enter [user@]hostname[:port]' + }); + + if (!host) { + return; + } + + const sshDest = new SSHDestination(host); + openRemoteSSHWindow(sshDest.toEncodedString(), reuseWindow); +} + +export function openRemoteSSHWindow(host: string, reuseWindow: boolean) { + vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: getRemoteAuthority(host), reuseWindow }); +} + +export function openRemoteSSHLocationWindow(host: string, path: string, reuseWindow: boolean) { + vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.from({ scheme: 'vscode-remote', authority: getRemoteAuthority(host), path }), { forceNewWindow: !reuseWindow }); +} + +export async function addNewHost() { + const sshConfigPath = getSSHConfigPath(); + if (!await fileExists(sshConfigPath)) { + await fs.promises.appendFile(sshConfigPath, ''); + } + + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(sshConfigPath), { preview: false }); + + const textEditor = vscode.window.activeTextEditor; + if (textEditor?.document.uri.fsPath !== sshConfigPath) { + return; + } + + const textDocument = textEditor.document; + const lastLine = textDocument.lineAt(textDocument.lineCount - 1); + + if (!lastLine.isEmptyOrWhitespace) { + await textEditor.edit((editBuilder: vscode.TextEditorEdit) => { + editBuilder.insert(lastLine.range.end, '\n'); + }); + } + + const snippet = '\nHost ${1:dev}\n\tHostName ${2:dev.example.com}\n\tUser ${3:john}'; + await textEditor.insertSnippet( + new vscode.SnippetString(snippet), + new vscode.Position(textDocument.lineCount, 0) + ); +} + +export async function openSSHConfigFile() { + const sshConfigPath = getSSHConfigPath(); + if (!await fileExists(sshConfigPath)) { + await fs.promises.appendFile(sshConfigPath, ''); + } + vscode.commands.executeCommand('vscode.open', vscode.Uri.file(sshConfigPath)); +} diff --git a/extensions/open-remote-ssh/src/common/disposable.ts b/extensions/open-remote-ssh/src/common/disposable.ts new file mode 100644 index 00000000..c81eb6cd --- /dev/null +++ b/extensions/open-remote-ssh/src/common/disposable.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; + +export function disposeAll(disposables: vscode.Disposable[]): void { + while (disposables.length) { + const item = disposables.pop(); + if (item) { + item.dispose(); + } + } +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: vscode.Disposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed(): boolean { + return this._isDisposed; + } +} diff --git a/extensions/open-remote-ssh/src/common/files.ts b/extensions/open-remote-ssh/src/common/files.ts new file mode 100644 index 00000000..6d9ff46f --- /dev/null +++ b/extensions/open-remote-ssh/src/common/files.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import * as os from 'os'; + +const homeDir = os.homedir(); + +export async function exists(path: string) { + try { + await fs.promises.access(path); + return true; + } catch { + return false; + } +} + +export function untildify(path: string) { + return path.replace(/^~(?=$|\/|\\)/, homeDir); +} + +export function normalizeToSlash(path: string) { + return path.replace(/\\/g, '/'); +} diff --git a/extensions/open-remote-ssh/src/common/logger.ts b/extensions/open-remote-ssh/src/common/logger.ts new file mode 100644 index 00000000..6489d0c0 --- /dev/null +++ b/extensions/open-remote-ssh/src/common/logger.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; + +type LogLevel = 'Trace' | 'Info' | 'Error'; + +export default class Log { + private output: vscode.OutputChannel; + + constructor(name: string) { + this.output = vscode.window.createOutputChannel(name); + } + + private data2String(data: any): string { + if (data instanceof Error) { + return data.stack || data.message; + } + if (data.success === false && data.message) { + return data.message; + } + return data.toString(); + } + + public trace(message: string, data?: any): void { + this.logLevel('Trace', message, data); + } + + public info(message: string, data?: any): void { + this.logLevel('Info', message, data); + } + + public error(message: string, data?: any): void { + this.logLevel('Error', message, data); + } + + public logLevel(level: LogLevel, message: string, data?: any): void { + this.output.appendLine(`[${level} - ${this.now()}] ${message}`); + if (data) { + this.output.appendLine(this.data2String(data)); + } + } + + private now(): string { + const now = new Date(); + return padLeft(now.getUTCHours() + '', 2, '0') + + ':' + padLeft(now.getMinutes() + '', 2, '0') + + ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds(); + } + + public show() { + this.output.show(); + } + + public dispose() { + this.output.dispose(); + } +} + +function padLeft(s: string, n: number, pad = ' ') { + return pad.repeat(Math.max(0, n - s.length)) + s; +} diff --git a/extensions/open-remote-ssh/src/common/platform.ts b/extensions/open-remote-ssh/src/common/platform.ts new file mode 100644 index 00000000..73c65ca9 --- /dev/null +++ b/extensions/open-remote-ssh/src/common/platform.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export const isWindows = process.platform === 'win32'; +export const isMacintosh = process.platform === 'darwin'; +export const isLinux = process.platform === 'linux'; diff --git a/extensions/open-remote-ssh/src/common/ports.ts b/extensions/open-remote-ssh/src/common/ports.ts new file mode 100644 index 00000000..e135441b --- /dev/null +++ b/extensions/open-remote-ssh/src/common/ports.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as net from 'net'; + +/** + * Finds a random unused port assigned by the operating system. Will reject in case no free port can be found. + */ +export function findRandomPort(): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer({ pauseOnConnect: true }); + server.on('error', reject); + server.on('listening', () => { + const port = (server.address() as net.AddressInfo).port; + server.close(() => resolve(port)); + }); + server.listen(0, '127.0.0.1'); + }); +} + +/** + * Given a start point and a max number of retries, will find a port that + * is openable. Will return 0 in case no free port can be found. + */ +export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise { + let done = false; + + return new Promise(resolve => { + const timeoutHandle = setTimeout(() => { + if (!done) { + done = true; + return resolve(0); + } + }, timeout); + + doFindFreePort(startPort, giveUpAfter, stride, (port) => { + if (!done) { + done = true; + clearTimeout(timeoutHandle); + return resolve(port); + } + }); + }); +} + +function doFindFreePort(startPort: number, giveUpAfter: number, stride: number, clb: (port: number) => void): void { + if (giveUpAfter === 0) { + return clb(0); + } + + const client = new net.Socket(); + + // If we can connect to the port it means the port is already taken so we continue searching + client.once('connect', () => { + dispose(client); + + return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb); + }); + + client.once('data', () => { + // this listener is required since node.js 8.x + }); + + client.once('error', (err: Error & { code?: string }) => { + dispose(client); + + // If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect + if (err.code !== 'ECONNREFUSED') { + return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb); + } + + // Otherwise it means the port is free to use! + return clb(startPort); + }); + + client.connect(startPort, '127.0.0.1'); +} + +/** + * Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener. + */ +export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number): Promise { + let resolved = false; + let timeoutHandle: NodeJS.Timeout | undefined = undefined; + let countTried = 1; + const server = net.createServer({ pauseOnConnect: true }); + function doResolve(port: number, resolve: (port: number) => void) { + if (!resolved) { + resolved = true; + server.removeAllListeners(); + server.close(); + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + resolve(port); + } + } + return new Promise(resolve => { + timeoutHandle = setTimeout(() => { + doResolve(0, resolve); + }, timeout); + + server.on('listening', () => { + doResolve(startPort, resolve); + }); + server.on('error', err => { + if (err && ((err).code === 'EADDRINUSE' || (err).code === 'EACCES') && (countTried < giveUpAfter)) { + startPort++; + countTried++; + server.listen(startPort, '127.0.0.1'); + } else { + doResolve(0, resolve); + } + }); + server.on('close', () => { + doResolve(0, resolve); + }); + server.listen(startPort, '127.0.0.1'); + }); +} + +function dispose(socket: net.Socket): void { + try { + socket.removeAllListeners('connect'); + socket.removeAllListeners('error'); + socket.end(); + socket.destroy(); + socket.unref(); + } catch (error) { + console.error(error); // otherwise this error would get lost in the callback chain + } +} diff --git a/extensions/open-remote-ssh/src/extension.ts b/extensions/open-remote-ssh/src/extension.ts new file mode 100644 index 00000000..e950f545 --- /dev/null +++ b/extensions/open-remote-ssh/src/extension.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import Log from './common/logger'; +import { RemoteSSHResolver, REMOTE_SSH_AUTHORITY } from './authResolver'; +import { openSSHConfigFile, promptOpenRemoteSSHWindow } from './commands'; +import { HostTreeDataProvider } from './hostTreeView'; +import { getRemoteWorkspaceLocationData, RemoteLocationHistory } from './remoteLocationHistory'; + +export async function activate(context: vscode.ExtensionContext) { + const logger = new Log('Remote - SSH'); + context.subscriptions.push(logger); + + const remoteSSHResolver = new RemoteSSHResolver(context, logger); + context.subscriptions.push(vscode.workspace.registerRemoteAuthorityResolver(REMOTE_SSH_AUTHORITY, remoteSSHResolver)); + context.subscriptions.push(remoteSSHResolver); + + const locationHistory = new RemoteLocationHistory(context); + const locationData = getRemoteWorkspaceLocationData(); + if (locationData) { + await locationHistory.addLocation(locationData[0], locationData[1]); + } + + const hostTreeDataProvider = new HostTreeDataProvider(locationHistory); + context.subscriptions.push(vscode.window.createTreeView('sshHosts', { treeDataProvider: hostTreeDataProvider })); + context.subscriptions.push(hostTreeDataProvider); + + context.subscriptions.push(vscode.commands.registerCommand('openremotessh.openEmptyWindow', () => promptOpenRemoteSSHWindow(false))); + context.subscriptions.push(vscode.commands.registerCommand('openremotessh.openEmptyWindowInCurrentWindow', () => promptOpenRemoteSSHWindow(true))); + context.subscriptions.push(vscode.commands.registerCommand('openremotessh.openConfigFile', () => openSSHConfigFile())); + context.subscriptions.push(vscode.commands.registerCommand('openremotessh.showLog', () => logger.show())); +} + +export function deactivate() { +} diff --git a/extensions/open-remote-ssh/src/hostTreeView.ts b/extensions/open-remote-ssh/src/hostTreeView.ts new file mode 100644 index 00000000..5874c53e --- /dev/null +++ b/extensions/open-remote-ssh/src/hostTreeView.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import * as path from 'path'; +import SSHConfiguration, { getSSHConfigPath } from './ssh/sshConfig'; +import { RemoteLocationHistory } from './remoteLocationHistory'; +import { Disposable } from './common/disposable'; +import { addNewHost, openRemoteSSHLocationWindow, openRemoteSSHWindow, openSSHConfigFile } from './commands'; +import SSHDestination from './ssh/sshDestination'; + +class HostItem { + constructor( + public hostname: string, + public locations: string[] + ) { + } +} + +class HostLocationItem { + constructor( + public path: string, + public hostname: string + ) { + } +} + +type DataTreeItem = HostItem | HostLocationItem; + +export class HostTreeDataProvider extends Disposable implements vscode.TreeDataProvider { + + private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter()); + public readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + constructor( + private locationHistory: RemoteLocationHistory + ) { + super(); + + this._register(vscode.commands.registerCommand('openremotessh.explorer.add', () => addNewHost())); + this._register(vscode.commands.registerCommand('openremotessh.explorer.configure', () => openSSHConfigFile())); + this._register(vscode.commands.registerCommand('openremotessh.explorer.refresh', () => this.refresh())); + this._register(vscode.commands.registerCommand('openremotessh.explorer.emptyWindowInNewWindow', e => this.openRemoteSSHWindow(e, false))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.emptyWindowInCurrentWindow', e => this.openRemoteSSHWindow(e, true))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.reopenFolderInNewWindow', e => this.openRemoteSSHLocationWindow(e, false))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.reopenFolderInCurrentWindow', e => this.openRemoteSSHLocationWindow(e, true))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.deleteFolderHistoryItem', e => this.deleteHostLocation(e))); + + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('remote.SSH.configFile')) { + this.refresh(); + } + })); + this._register(vscode.workspace.onDidSaveTextDocument(e => { + if (e.uri.fsPath === getSSHConfigPath()) { + this.refresh(); + } + })); + } + + getTreeItem(element: DataTreeItem): vscode.TreeItem { + if (element instanceof HostLocationItem) { + const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)'); + const treeItem = new vscode.TreeItem(label); + treeItem.description = path.posix.dirname(element.path); + treeItem.iconPath = new vscode.ThemeIcon('folder'); + treeItem.contextValue = 'openremotessh.explorer.folder'; + return treeItem; + } + + const treeItem = new vscode.TreeItem(element.hostname); + treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; + treeItem.iconPath = new vscode.ThemeIcon('vm'); + treeItem.contextValue = 'openremotessh.explorer.host'; + return treeItem; + } + + async getChildren(element?: HostItem): Promise { + if (!element) { + const sshConfigFile = await SSHConfiguration.loadFromFS(); + const hosts = sshConfigFile.getAllConfiguredHosts(); + return hosts.map(hostname => new HostItem(hostname, this.locationHistory.getHistory(hostname))); + } + if (element instanceof HostItem) { + return element.locations.map(location => new HostLocationItem(location, element.hostname)); + } + return []; + } + + private refresh() { + this._onDidChangeTreeData.fire(); + } + + private async deleteHostLocation(element: HostLocationItem) { + await this.locationHistory.removeLocation(element.hostname, element.path); + this.refresh(); + } + + private async openRemoteSSHWindow(element: HostItem, reuseWindow: boolean) { + const sshDest = new SSHDestination(element.hostname); + openRemoteSSHWindow(sshDest.toEncodedString(), reuseWindow); + } + + private async openRemoteSSHLocationWindow(element: HostLocationItem, reuseWindow: boolean) { + const sshDest = new SSHDestination(element.hostname); + openRemoteSSHLocationWindow(sshDest.toEncodedString(), element.path, reuseWindow); + } +} diff --git a/extensions/open-remote-ssh/src/remoteLocationHistory.ts b/extensions/open-remote-ssh/src/remoteLocationHistory.ts new file mode 100644 index 00000000..6e6b7777 --- /dev/null +++ b/extensions/open-remote-ssh/src/remoteLocationHistory.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import { REMOTE_SSH_AUTHORITY } from './authResolver'; +import SSHDestination from './ssh/sshDestination'; + +export class RemoteLocationHistory { + private static STORAGE_KEY = 'remoteLocationHistory_v0'; + + private remoteLocationHistory: Record = {}; + + constructor(private context: vscode.ExtensionContext) { + // context.globalState.update(RemoteLocationHistory.STORAGE_KEY, undefined); + this.remoteLocationHistory = context.globalState.get(RemoteLocationHistory.STORAGE_KEY) || {}; + } + + getHistory(host: string): string[] { + return this.remoteLocationHistory[host] || []; + } + + async addLocation(host: string, path: string) { + const hostLocations = this.remoteLocationHistory[host] || []; + if (!hostLocations.includes(path)) { + hostLocations.unshift(path); + this.remoteLocationHistory[host] = hostLocations; + + await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory); + } + } + + async removeLocation(host: string, path: string) { + let hostLocations = this.remoteLocationHistory[host] || []; + hostLocations = hostLocations.filter(l => l !== path); + this.remoteLocationHistory[host] = hostLocations; + + await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory); + } +} + +export function getRemoteWorkspaceLocationData(): [string, string] | undefined { + let location = vscode.workspace.workspaceFile; + if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_SSH_AUTHORITY) && location.path.endsWith('.code-workspace')) { + const [, host] = location.authority.split('+'); + const sshDest = SSHDestination.parseEncoded(host); + return [sshDest.hostname, location.path]; + } + + location = vscode.workspace.workspaceFolders?.[0].uri; + if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_SSH_AUTHORITY)) { + const [, host] = location.authority.split('+'); + const sshDest = SSHDestination.parseEncoded(host); + return [sshDest.hostname, location.path]; + } + + return undefined; +} diff --git a/extensions/open-remote-ssh/src/serverConfig.ts b/extensions/open-remote-ssh/src/serverConfig.ts new file mode 100644 index 00000000..3f2758fa --- /dev/null +++ b/extensions/open-remote-ssh/src/serverConfig.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +let vscodeProductJson: any; +async function getVSCodeProductJson() { + if (!vscodeProductJson) { + const productJsonStr = await fs.promises.readFile(path.join(vscode.env.appRoot, 'product.json'), 'utf8'); + vscodeProductJson = JSON.parse(productJsonStr); + } + + return vscodeProductJson; +} + +export interface IServerConfig { + version: string; + commit: string; + quality: string; + release?: string; // void-like specific + serverApplicationName: string; + serverDataFolderName: string; + serverDownloadUrlTemplate?: string; // void-like specific +} + +export async function getVSCodeServerConfig(): Promise { + const productJson = await getVSCodeProductJson(); + + const customServerBinaryName = vscode.workspace.getConfiguration('remote.SSH.experimental').get('serverBinaryName', ''); + + return { + version: vscode.version.replace('-insider', ''), + commit: productJson.commit, + quality: productJson.quality, + release: productJson.release, + serverApplicationName: customServerBinaryName || productJson.serverApplicationName, + serverDataFolderName: productJson.serverDataFolderName, + serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate + }; +} diff --git a/extensions/open-remote-ssh/src/serverSetup.ts b/extensions/open-remote-ssh/src/serverSetup.ts new file mode 100644 index 00000000..d851988a --- /dev/null +++ b/extensions/open-remote-ssh/src/serverSetup.ts @@ -0,0 +1,626 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as crypto from 'crypto'; +import Log from './common/logger'; +import { getVSCodeServerConfig } from './serverConfig'; +import SSHConnection from './ssh/sshConnection'; + +export interface ServerInstallOptions { + id: string; + quality: string; + commit: string; + version: string; + release?: string; // void specific + extensionIds: string[]; + envVariables: string[]; + useSocketPath: boolean; + serverApplicationName: string; + serverDataFolderName: string; + serverDownloadUrlTemplate: string; +} + +export interface ServerInstallResult { + exitCode: number; + listeningOn: number | string; + connectionToken: string; + logFile: string; + osReleaseId: string; + arch: string; + platform: string; + tmpDir: string; + [key: string]: any; +} + +export class ServerInstallError extends Error { + constructor(message: string) { + super(message); + } +} + +const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/${NAME_OF_REPO}/releases/download/${version}.${release}/void-server-${os}-${arch}-${version}.${release}.tar.gz'; + +export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise { + let shell = 'powershell'; + + // detect platform and shell for windows + if (!platform || platform === 'windows') { + const result = await conn.exec('uname -s'); + + if (result.stdout) { + if (result.stdout.includes('windows32')) { + platform = 'windows'; + } else if (result.stdout.includes('MINGW64')) { + platform = 'windows'; + shell = 'bash'; + } + } else if (result.stderr) { + if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) { + platform = 'windows'; + } + + if (result.stderr.includes('is not recognized as an internal or external command')) { + platform = 'windows'; + shell = 'cmd'; + } + } + + if (platform) { + logger.trace(`Detected platform: ${platform}, ${shell}`); + } + } + + const scriptId = crypto.randomBytes(12).toString('hex'); + + const vscodeServerConfig = await getVSCodeServerConfig(); + const installOptions: ServerInstallOptions = { + id: scriptId, + version: vscodeServerConfig.version, + commit: vscodeServerConfig.commit, + quality: vscodeServerConfig.quality, + release: vscodeServerConfig.release, + extensionIds, + envVariables, + useSocketPath, + serverApplicationName: vscodeServerConfig.serverApplicationName, + serverDataFolderName: vscodeServerConfig.serverDataFolderName, + serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE, + }; + + let commandOutput: { stdout: string; stderr: string }; + if (platform === 'windows') { + const installServerScript = generatePowerShellInstallScript(installOptions); + + logger.trace('Server install command:', installServerScript); + + const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`; + const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`; + const endRegex = new RegExp(`${scriptId}: end`); + // investigate if it's possible to use `-EncodedCommand` flag + // https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/ + let command = ''; + if (shell === 'powershell') { + command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`; + } else if (shell === 'bash') { + command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`; + } else if (shell === 'cmd') { + const script = installServerScript.trim() + // remove comments + .replace(/^#.*$/gm, '') + // remove empty lines + .replace(/\n{2,}/gm, '\n') + // remove leading spaces + .replace(/^\s*/gm, '') + // escape double quotes (from powershell/cmd) + .replace(/"/g, '"""') + // escape single quotes (from cmd) + .replace(/'/g, `''`) + // escape redirect (from cmd) + .replace(/>/g, `^>`) + // escape new lines (from powershell/cmd) + .replace(/\n/g, '\'`n\''); + + command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`; + + logger.trace('Command length (8191 max):', command.length); + + if (command.length > 8191) { + throw new ServerInstallError(`Command line too long`); + } + } else { + throw new ServerInstallError(`Not supported shell: ${shell}`); + } + + commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout)); + } else { + const installServerScript = generateBashInstallScript(installOptions); + + logger.trace('Server install command:', installServerScript); + // Fish shell does not support heredoc so let's workaround it using -c option, + // also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676 + commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`); + } + + if (commandOutput.stderr) { + logger.trace('Server install command stderr:', commandOutput.stderr); + } + logger.trace('Server install command stdout:', commandOutput.stdout); + + const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId); + if (!resultMap) { + throw new ServerInstallError(`Failed parsing install script output`); + } + + const exitCode = parseInt(resultMap.exitCode, 10); + if (exitCode !== 0) { + throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`); + } + + const listeningOn = resultMap.listeningOn.match(/^\d+$/) + ? parseInt(resultMap.listeningOn, 10) + : resultMap.listeningOn; + + const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key))); + + return { + exitCode, + listeningOn, + connectionToken: resultMap.connectionToken, + logFile: resultMap.logFile, + osReleaseId: resultMap.osReleaseId, + arch: resultMap.arch, + platform: resultMap.platform, + tmpDir: resultMap.tmpDir, + ...remoteEnvVars + }; +} + +function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined { + const startResultStr = `${scriptId}: start`; + const endResultStr = `${scriptId}: end`; + + const startResultIdx = str.indexOf(startResultStr); + if (startResultIdx < 0) { + return undefined; + } + + const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length); + if (endResultIdx < 0) { + return undefined; + } + + const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx); + + const resultMap: { [k: string]: string } = {}; + const resultArr = installResult.split(/\r?\n/); + for (const line of resultArr) { + const [key, value] = line.split('=='); + resultMap[key] = value; + } + + return resultMap; +} + +function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) { + const extensions = extensionIds.map(id => '--install-extension ' + id).join(' '); + return ` +# Server installation script + +TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}" + +DISTRO_VERSION="${version}" +DISTRO_COMMIT="${commit}" +DISTRO_QUALITY="${quality}" +DISTRO_VOID_RELEASE="${release ?? ''}" + +SERVER_APP_NAME="${serverApplicationName}" +SERVER_INITIAL_EXTENSIONS="${extensions}" +SERVER_LISTEN_FLAG="${useSocketPath ? `--socket-path="$TMP_DIR/vscode-server-sock-${crypto.randomUUID()}"` : '--port=0'}" +SERVER_DATA_DIR="$HOME/${serverDataFolderName}" +SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT" +SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME" +SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log" +SERVER_PIDFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.pid" +SERVER_TOKENFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.token" +SERVER_ARCH= +SERVER_CONNECTION_TOKEN= +SERVER_DOWNLOAD_URL= + +LISTENING_ON= +OS_RELEASE_ID= +ARCH= +PLATFORM= + +# Mimic output from logs of remote-ssh extension +print_install_results_and_exit() { + echo "${id}: start" + echo "exitCode==$1==" + echo "listeningOn==$LISTENING_ON==" + echo "connectionToken==$SERVER_CONNECTION_TOKEN==" + echo "logFile==$SERVER_LOGFILE==" + echo "osReleaseId==$OS_RELEASE_ID==" + echo "arch==$ARCH==" + echo "platform==$PLATFORM==" + echo "tmpDir==$TMP_DIR==" + ${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')} + echo "${id}: end" + exit 0 +} + +# Check if platform is supported +KERNEL="$(uname -s)" +case $KERNEL in + Darwin) + PLATFORM="darwin" + ;; + Linux) + PLATFORM="linux" + ;; + FreeBSD) + PLATFORM="freebsd" + ;; + DragonFly) + PLATFORM="dragonfly" + ;; + *) + echo "Error platform not supported: $KERNEL" + print_install_results_and_exit 1 + ;; +esac + +# Check machine architecture +ARCH="$(uname -m)" +case $ARCH in + x86_64 | amd64) + SERVER_ARCH="x64" + ;; + armv7l | armv8l) + SERVER_ARCH="armhf" + ;; + arm64 | aarch64) + SERVER_ARCH="arm64" + ;; + ppc64le) + SERVER_ARCH="ppc64le" + ;; + riscv64) + SERVER_ARCH="riscv64" + ;; + loongarch64) + SERVER_ARCH="loong64" + ;; + s390x) + SERVER_ARCH="s390x" + ;; + *) + echo "Error architecture not supported: $ARCH" + print_install_results_and_exit 1 + ;; +esac + +# https://www.freedesktop.org/software/systemd/man/os-release.html +OS_RELEASE_ID="$(grep -i '^ID=' /etc/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')" +if [[ -z $OS_RELEASE_ID ]]; then + OS_RELEASE_ID="$(grep -i '^ID=' /usr/lib/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')" + if [[ -z $OS_RELEASE_ID ]]; then + OS_RELEASE_ID="unknown" + fi +fi + +# Create installation folder +if [[ ! -d $SERVER_DIR ]]; then + mkdir -p $SERVER_DIR + if (( $? > 0 )); then + echo "Error creating server install directory" + print_install_results_and_exit 1 + fi +fi + +# adjust platform for void download, if needed +if [[ $OS_RELEASE_ID = alpine ]]; then + PLATFORM=$OS_RELEASE_ID +fi + +SERVER_DOWNLOAD_URL="$(echo "${serverDownloadUrlTemplate.replace(/\$\{/g, '\\${')}" | sed "s/\\\${quality}/$DISTRO_QUALITY/g" | sed "s/\\\${version}/$DISTRO_VERSION/g" | sed "s/\\\${commit}/$DISTRO_COMMIT/g" | sed "s/\\\${os}/$PLATFORM/g" | sed "s/\\\${arch}/$SERVER_ARCH/g" | sed "s/\\\${release}/$DISTRO_VOID_RELEASE/g")" + +# Check if server script is already installed +if [[ ! -f $SERVER_SCRIPT ]]; then + case "$PLATFORM" in + darwin | linux | alpine ) + ;; + *) + echo "Error '$PLATFORM' needs manual installation of remote extension host" + print_install_results_and_exit 1 + ;; + esac + + pushd $SERVER_DIR > /dev/null + + if [[ ! -z $(which wget) ]]; then + wget --tries=3 --timeout=10 --continue --no-verbose -O vscode-server.tar.gz $SERVER_DOWNLOAD_URL + elif [[ ! -z $(which curl) ]]; then + curl --retry 3 --connect-timeout 10 --location --show-error --silent --output vscode-server.tar.gz $SERVER_DOWNLOAD_URL + else + echo "Error no tool to download server binary" + print_install_results_and_exit 1 + fi + + if (( $? > 0 )); then + echo "Error downloading server from $SERVER_DOWNLOAD_URL" + print_install_results_and_exit 1 + fi + + tar -xf vscode-server.tar.gz --strip-components 1 + if (( $? > 0 )); then + echo "Error while extracting server contents" + print_install_results_and_exit 1 + fi + + if [[ ! -f $SERVER_SCRIPT ]]; then + echo "Error server contents are corrupted" + print_install_results_and_exit 1 + fi + + rm -f vscode-server.tar.gz + + popd > /dev/null +else + echo "Server script already installed in $SERVER_SCRIPT" +fi + +# Try to find if server is already running +if [[ -f $SERVER_PIDFILE ]]; then + SERVER_PID="$(cat $SERVER_PIDFILE)" + SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)" +else + SERVER_RUNNING_PROCESS="$(ps -o pid,args -A | grep $SERVER_SCRIPT | grep -v grep)" +fi + +if [[ -z $SERVER_RUNNING_PROCESS ]]; then + if [[ -f $SERVER_LOGFILE ]]; then + rm $SERVER_LOGFILE + fi + if [[ -f $SERVER_TOKENFILE ]]; then + rm $SERVER_TOKENFILE + fi + + touch $SERVER_TOKENFILE + chmod 600 $SERVER_TOKENFILE + SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}" + echo $SERVER_CONNECTION_TOKEN > $SERVER_TOKENFILE + + $SERVER_SCRIPT --start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --enable-remote-auto-shutdown --accept-server-license-terms &> $SERVER_LOGFILE & + echo $! > $SERVER_PIDFILE +else + echo "Server script is already running $SERVER_SCRIPT" +fi + +if [[ -f $SERVER_TOKENFILE ]]; then + SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)" +else + echo "Error server token file not found $SERVER_TOKENFILE" + print_install_results_and_exit 1 +fi + +if [[ -f $SERVER_LOGFILE ]]; then + for i in {1..5}; do + LISTENING_ON="$(cat $SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')" + if [[ -n $LISTENING_ON ]]; then + break + fi + sleep 0.5 + done + + if [[ -z $LISTENING_ON ]]; then + echo "Error server did not start successfully" + print_install_results_and_exit 1 + fi +else + echo "Error server log file not found $SERVER_LOGFILE" + print_install_results_and_exit 1 +fi + +# Finish server setup +print_install_results_and_exit 0 +`; +} + +function generatePowerShellInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) { + const extensions = extensionIds.map(id => '--install-extension ' + id).join(' '); + const downloadUrl = serverDownloadUrlTemplate + .replace(/\$\{quality\}/g, quality) + .replace(/\$\{version\}/g, version) + .replace(/\$\{commit\}/g, commit) + .replace(/\$\{os\}/g, 'win32') + .replace(/\$\{arch\}/g, 'x64') + .replace(/\$\{release\}/g, release ?? ''); + + return ` +# Server installation script + +$TMP_DIR="$env:TEMP\\$([System.IO.Path]::GetRandomFileName())" +$ProgressPreference = "SilentlyContinue" + +$DISTRO_VERSION="${version}" +$DISTRO_COMMIT="${commit}" +$DISTRO_QUALITY="${quality}" +$DISTRO_VOID_RELEASE="${release ?? ''}" + +$SERVER_APP_NAME="${serverApplicationName}" +$SERVER_INITIAL_EXTENSIONS="${extensions}" +$SERVER_LISTEN_FLAG="${useSocketPath ? `--socket-path="$TMP_DIR/vscode-server-sock-${crypto.randomUUID()}"` : '--port=0'}" +$SERVER_DATA_DIR="$(Resolve-Path ~)\\${serverDataFolderName}" +$SERVER_DIR="$SERVER_DATA_DIR\\bin\\$DISTRO_COMMIT" +$SERVER_SCRIPT="$SERVER_DIR\\bin\\$SERVER_APP_NAME.cmd" +$SERVER_LOGFILE="$SERVER_DATA_DIR\\.$DISTRO_COMMIT.log" +$SERVER_PIDFILE="$SERVER_DATA_DIR\\.$DISTRO_COMMIT.pid" +$SERVER_TOKENFILE="$SERVER_DATA_DIR\\.$DISTRO_COMMIT.token" +$SERVER_ARCH= +$SERVER_CONNECTION_TOKEN= +$SERVER_DOWNLOAD_URL= + +$LISTENING_ON= +$OS_RELEASE_ID= +$ARCH= +$PLATFORM="win32" + +function printInstallResults($code) { + "${id}: start" + "exitCode==$code==" + "listeningOn==$LISTENING_ON==" + "connectionToken==$SERVER_CONNECTION_TOKEN==" + "logFile==$SERVER_LOGFILE==" + "osReleaseId==$OS_RELEASE_ID==" + "arch==$ARCH==" + "platform==$PLATFORM==" + "tmpDir==$TMP_DIR==" + ${envVariables.map(envVar => `"${envVar}==$${envVar}=="`).join('\n')} + "${id}: end" +} + +# Check machine architecture +$ARCH=$env:PROCESSOR_ARCHITECTURE +# Use x64 version for ARM64, as it's not yet available. +if(($ARCH -eq "AMD64") -or ($ARCH -eq "IA64") -or ($ARCH -eq "ARM64")) { + $SERVER_ARCH="x64" +} +else { + "Error architecture not supported: $ARCH" + printInstallResults 1 + exit 0 +} + +# Create installation folder +if(!(Test-Path $SERVER_DIR)) { + try { + ni -it d $SERVER_DIR -f -ea si + } catch { + "Error creating server install directory - $($_.ToString())" + exit 1 + } + + if(!(Test-Path $SERVER_DIR)) { + "Error creating server install directory" + exit 1 + } +} + +cd $SERVER_DIR + +# Check if server script is already installed +if(!(Test-Path $SERVER_SCRIPT)) { + del vscode-server.tar.gz + + $REQUEST_ARGUMENTS = @{ + Uri="${downloadUrl}" + TimeoutSec=20 + OutFile="vscode-server.tar.gz" + UseBasicParsing=$True + } + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + Invoke-RestMethod @REQUEST_ARGUMENTS + + if(Test-Path "vscode-server.tar.gz") { + tar -xf vscode-server.tar.gz --strip-components 1 + + del vscode-server.tar.gz + } + + if(!(Test-Path $SERVER_SCRIPT)) { + "Error while installing the server binary" + exit 1 + } +} +else { + "Server script already installed in $SERVER_SCRIPT" +} + +# Try to find if server is already running +if(Get-Process node -ErrorAction SilentlyContinue | Where-Object Path -Like "$SERVER_DIR\\*") { + echo "Server script is already running $SERVER_SCRIPT" +} +else { + if(Test-Path $SERVER_LOGFILE) { + del $SERVER_LOGFILE + } + if(Test-Path $SERVER_PIDFILE) { + del $SERVER_PIDFILE + } + if(Test-Path $SERVER_TOKENFILE) { + del $SERVER_TOKENFILE + } + + $SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}" + [System.IO.File]::WriteAllLines($SERVER_TOKENFILE, $SERVER_CONNECTION_TOKEN) + + $SCRIPT_ARGUMENTS="--start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --enable-remote-auto-shutdown --accept-server-license-terms *> '$SERVER_LOGFILE'" + + $START_ARGUMENTS = @{ + FilePath = "powershell.exe" + WindowStyle = "hidden" + ArgumentList = @( + "-ExecutionPolicy", "Unrestricted", "-NoLogo", "-NoProfile", "-NonInteractive", "-c", "$SERVER_SCRIPT $SCRIPT_ARGUMENTS" + ) + PassThru = $True + } + + $SERVER_ID = (start @START_ARGUMENTS).ID + + if($SERVER_ID) { + [System.IO.File]::WriteAllLines($SERVER_PIDFILE, $SERVER_ID) + } +} + +if(Test-Path $SERVER_TOKENFILE) { + $SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)" +} +else { + "Error server token file not found $SERVER_TOKENFILE" + printInstallResults 1 + exit 0 +} + +sleep -Milliseconds 500 + +$SELECT_ARGUMENTS = @{ + Path = $SERVER_LOGFILE + Pattern = "Extension host agent listening on (\\d+)" +} + +for($I = 1; $I -le 5; $I++) { + if(Test-Path $SERVER_LOGFILE) { + $GROUPS = (Select-String @SELECT_ARGUMENTS).Matches.Groups + + if($GROUPS) { + $LISTENING_ON = $GROUPS[1].Value + break + } + } + + sleep -Milliseconds 500 +} + +if(!(Test-Path $SERVER_LOGFILE)) { + "Error server log file not found $SERVER_LOGFILE" + printInstallResults 1 + exit 0 +} + +# Finish server setup +printInstallResults 0 + +if($SERVER_ID) { + while($True) { + if(!(gps -Id $SERVER_ID)) { + "server died, exit" + exit 0 + } + + sleep 30 + } +} +`; +} diff --git a/extensions/open-remote-ssh/src/ssh/hostfile.ts b/extensions/open-remote-ssh/src/ssh/hostfile.ts new file mode 100644 index 00000000..51d34a9e --- /dev/null +++ b/extensions/open-remote-ssh/src/ssh/hostfile.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import { exists as folderExists } from '../common/files'; + +const PATH_SSH_USER_DIR = path.join(os.homedir(), '.ssh'); +const KNOW_HOST_FILE = path.join(PATH_SSH_USER_DIR, 'known_hosts'); +const HASH_MAGIC = '|1|'; +const HASH_DELIM = '|'; + +export async function checkNewHostInHostkeys(host: string): Promise { + const fileContent = await fs.promises.readFile(KNOW_HOST_FILE, { encoding: 'utf8' }); + const lines = fileContent.split(/\r?\n/); + for (let line of lines) { + line = line.trim(); + if (!line.startsWith(HASH_MAGIC)) { + continue; + } + + const [hostEncripted_] = line.split(' '); + const [salt_, hostHash_] = hostEncripted_.substring(HASH_MAGIC.length).split(HASH_DELIM); + const hostHash = crypto.createHmac('sha1', Buffer.from(salt_, 'base64')).update(host).digest(); + if (hostHash.toString('base64') === hostHash_) { + return false; + } + } + + return true; +} + +export async function addHostToHostFile(host: string, hostKey: Buffer, type: string): Promise { + if (!folderExists(PATH_SSH_USER_DIR)) { + await fs.promises.mkdir(PATH_SSH_USER_DIR, 0o700); + } + + const salt = crypto.randomBytes(20); + const hostHash = crypto.createHmac('sha1', salt).update(host).digest(); + + const entry = `${HASH_MAGIC}${salt.toString('base64')}${HASH_DELIM}${hostHash.toString('base64')} ${type} ${hostKey.toString('base64')}\n`; + await fs.promises.appendFile(KNOW_HOST_FILE, entry); +} diff --git a/extensions/open-remote-ssh/src/ssh/identityFiles.ts b/extensions/open-remote-ssh/src/ssh/identityFiles.ts new file mode 100644 index 00000000..ffb33161 --- /dev/null +++ b/extensions/open-remote-ssh/src/ssh/identityFiles.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import type { ParsedKey } from 'ssh2-streams'; +import * as ssh2 from 'ssh2'; +import { untildify, exists as fileExists } from '../common/files'; +import Log from '../common/logger'; + +const homeDir = os.homedir(); +const PATH_SSH_CLIENT_ID_DSA = path.join(homeDir, '.ssh', '/id_dsa'); +const PATH_SSH_CLIENT_ID_ECDSA = path.join(homeDir, '.ssh', '/id_ecdsa'); +const PATH_SSH_CLIENT_ID_RSA = path.join(homeDir, '.ssh', '/id_rsa'); +const PATH_SSH_CLIENT_ID_ED25519 = path.join(homeDir, '.ssh', '/id_ed25519'); +const PATH_SSH_CLIENT_ID_XMSS = path.join(homeDir, '.ssh', '/id_xmss'); +const PATH_SSH_CLIENT_ID_ECDSA_SK = path.join(homeDir, '.ssh', '/id_ecdsa_sk'); +const PATH_SSH_CLIENT_ID_ED25519_SK = path.join(homeDir, '.ssh', '/id_ed25519_sk'); + +const DEFAULT_IDENTITY_FILES: string[] = [ + PATH_SSH_CLIENT_ID_RSA, + PATH_SSH_CLIENT_ID_ECDSA, + PATH_SSH_CLIENT_ID_ECDSA_SK, + PATH_SSH_CLIENT_ID_ED25519, + PATH_SSH_CLIENT_ID_ED25519_SK, + PATH_SSH_CLIENT_ID_XMSS, + PATH_SSH_CLIENT_ID_DSA, +]; + +export interface SSHKey { + filename: string; + parsedKey: ParsedKey; + fingerprint: string; + agentSupport?: boolean; + isPrivate?: boolean; +} + +// From https://github.com/openssh/openssh-portable/blob/acb2059febaddd71ee06c2ebf63dcf211d9ab9f2/sshconnect2.c#L1689-L1690 +export async function gatherIdentityFiles(identityFiles: string[], sshAgentSock: string | undefined, identitiesOnly: boolean, logger: Log) { + identityFiles = identityFiles.map(untildify).map(i => i.replace(/\.pub$/, '')); + if (identityFiles.length === 0) { + identityFiles.push(...DEFAULT_IDENTITY_FILES); + } + + const identityFileContentsResult = await Promise.allSettled(identityFiles.map(async keyPath => { + keyPath = await fileExists(keyPath + '.pub') ? keyPath + '.pub' : keyPath; + return fs.promises.readFile(keyPath); + })); + const fileKeys: SSHKey[] = identityFileContentsResult.map((result, i) => { + if (result.status === 'rejected') { + return undefined; + } + + const parsedResult = ssh2.utils.parseKey(result.value); + if (parsedResult instanceof Error || !parsedResult) { + logger.error(`Error while parsing SSH public key ${identityFiles[i]}:`, parsedResult); + return undefined; + } + + const parsedKey = Array.isArray(parsedResult) ? parsedResult[0] : parsedResult; + const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64'); + + return { + filename: identityFiles[i], + parsedKey, + fingerprint + }; + }).filter((v: T | undefined): v is T => !!v); + + let sshAgentParsedKeys: ParsedKey[] = []; + try { + if (!sshAgentSock) { + throw new Error(`SSH_AUTH_SOCK environment variable not defined`); + } + + sshAgentParsedKeys = await new Promise((resolve, reject) => { + const sshAgent = new ssh2.OpenSSHAgent(sshAgentSock); + sshAgent.getIdentities((err, publicKeys) => { + if (err) { + reject(err); + } else { + resolve(publicKeys || []); + } + }); + }); + } catch (e) { + logger.error(`Couldn't get identities from OpenSSH agent`, e); + } + + const sshAgentKeys: SSHKey[] = sshAgentParsedKeys.map(parsedKey => { + const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64'); + return { + filename: parsedKey.comment, + parsedKey, + fingerprint, + agentSupport: true + }; + }); + + const agentKeys: SSHKey[] = []; + const preferredIdentityKeys: SSHKey[] = []; + for (const agentKey of sshAgentKeys) { + const foundIdx = fileKeys.findIndex(k => agentKey.parsedKey.type === k.parsedKey.type && agentKey.fingerprint === k.fingerprint); + if (foundIdx >= 0) { + preferredIdentityKeys.push({ ...fileKeys[foundIdx], agentSupport: true }); + fileKeys.splice(foundIdx, 1); + } else if (!identitiesOnly) { + agentKeys.push(agentKey); + } + } + preferredIdentityKeys.push(...agentKeys); + preferredIdentityKeys.push(...fileKeys); + + logger.trace(`Identity keys:`, preferredIdentityKeys.length ? preferredIdentityKeys.map(k => `${k.filename} ${k.parsedKey.type} SHA256:${k.fingerprint}`).join('\n') : 'None'); + + return preferredIdentityKeys; +} diff --git a/extensions/open-remote-ssh/src/ssh/sshConfig.ts b/extensions/open-remote-ssh/src/ssh/sshConfig.ts new file mode 100644 index 00000000..6ce6ed32 --- /dev/null +++ b/extensions/open-remote-ssh/src/ssh/sshConfig.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; +import SSHConfig, { Directive, Line, Section } from '@jeanp413/ssh-config'; +import * as vscode from 'vscode'; +import { exists as fileExists, normalizeToSlash, untildify } from '../common/files'; +import { isWindows } from '../common/platform'; +import { glob } from 'glob'; + +const systemSSHConfig = isWindows ? path.resolve(process.env.ALLUSERSPROFILE || 'C:\\ProgramData', 'ssh\\ssh_config') : '/etc/ssh/ssh_config'; +const defaultSSHConfigPath = path.resolve(os.homedir(), '.ssh/config'); + +export function getSSHConfigPath() { + const sshConfigPath = vscode.workspace.getConfiguration('remote.SSH').get('configFile'); + return sshConfigPath ? untildify(sshConfigPath) : defaultSSHConfigPath; +} + +function isDirective(line: Line): line is Directive { + return line.type === SSHConfig.DIRECTIVE; +} + +function isHostSection(line: Line): line is Section { + return isDirective(line) && line.param === 'Host' && !!line.value && !!(line as Section).config; +} + +function isIncludeDirective(line: Line): line is Section { + return isDirective(line) && line.param === 'Include' && !!line.value; +} + +const SSH_CONFIG_PROPERTIES: Record = { + 'host': 'Host', + 'hostname': 'HostName', + 'user': 'User', + 'port': 'Port', + 'identityagent': 'IdentityAgent', + 'identitiesonly': 'IdentitiesOnly', + 'identityfile': 'IdentityFile', + 'forwardagent': 'ForwardAgent', + 'preferredauthentications': 'PreferredAuthentications', + 'proxyjump': 'ProxyJump', + 'proxycommand': 'ProxyCommand', + 'include': 'Include', +}; + +function normalizeProp(prop: Directive) { + prop.param = SSH_CONFIG_PROPERTIES[prop.param.toLowerCase()] || prop.param; +} + +function normalizeSSHConfig(config: SSHConfig) { + for (const line of config) { + if (isDirective(line)) { + normalizeProp(line); + } + if (isHostSection(line)) { + normalizeSSHConfig(line.config); + } + } + return config; +} + +async function parseSSHConfigFromFile(filePath: string, userConfig: boolean) { + let content = ''; + if (await fileExists(filePath)) { + content = (await fs.promises.readFile(filePath, 'utf8')).trim(); + } + const config = normalizeSSHConfig(SSHConfig.parse(content)); + + const includedConfigs: [number, SSHConfig[]][] = []; + for (let i = 0; i < config.length; i++) { + const line = config[i]; + if (isIncludeDirective(line)) { + const values = (line.value as string).split(',').map(s => s.trim()); + const configs: SSHConfig[] = []; + for (const value of values) { + const includePaths = await glob(normalizeToSlash(untildify(value)), { + absolute: true, + cwd: normalizeToSlash(path.dirname(userConfig ? defaultSSHConfigPath : systemSSHConfig)) + }); + for (const p of includePaths) { + configs.push(await parseSSHConfigFromFile(p, userConfig)); + } + } + includedConfigs.push([i, configs]); + } + } + for (const [idx, includeConfigs] of includedConfigs.reverse()) { + config.splice(idx, 1, ...includeConfigs.flat()); + } + + return config; +} + +export default class SSHConfiguration { + + static async loadFromFS(): Promise { + const config = await parseSSHConfigFromFile(getSSHConfigPath(), true); + config.push(...await parseSSHConfigFromFile(systemSSHConfig, false)); + + return new SSHConfiguration(config); + } + + constructor(private sshConfig: SSHConfig) { + } + + getAllConfiguredHosts(): string[] { + const hosts = new Set(); + for (const line of this.sshConfig) { + if (isHostSection(line)) { + const value = Array.isArray(line.value) ? line.value[0] : line.value; + const isPattern = /^!/.test(value) || /[?*]/.test(value); + if (!isPattern) { + hosts.add(value); + } + } + } + + return [...hosts.keys()]; + } + + getHostConfiguration(host: string): Record { + // Only a few directives return an array + // https://github.com/jeanp413/ssh-config/blob/8d187bb8f1d83a51ff2b1d127e6b6269d24092b5/src/ssh-config.ts#L9C1-L9C118 + return this.sshConfig.compute(host) as Record; + } +} diff --git a/extensions/open-remote-ssh/src/ssh/sshConnection.ts b/extensions/open-remote-ssh/src/ssh/sshConnection.ts new file mode 100644 index 00000000..abaf1471 --- /dev/null +++ b/extensions/open-remote-ssh/src/ssh/sshConnection.ts @@ -0,0 +1,367 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EventEmitter } from 'events'; +import * as net from 'net'; +import * as fs from 'fs'; +import * as stream from 'stream'; +import { Client, ClientChannel, ClientErrorExtensions, ExecOptions, ShellOptions, ConnectConfig } from 'ssh2'; +import { Server } from 'net'; +import { SocksConnectionInfo, createServer as createSocksServer } from 'simple-socks'; + +export interface SSHConnectConfig extends ConnectConfig { + /** Optional Unique ID attached to ssh connection. */ + uniqueId?: string; + /** Automatic retry to connect, after disconnect. Default true */ + reconnect?: boolean; + /** Number of reconnect retry, after disconnect. Default 10 */ + reconnectTries?: number; + /** Delay after which reconnect should be done. Default 5000ms */ + reconnectDelay?: number; + /** Path to private key */ + identity?: string | Buffer; +} + +export interface SSHTunnelConfig { + /** Remote Address to connect */ + remoteAddr?: string; + /** Local port to bind to. By default, it will bind to a random port, if not passed */ + localPort?: number; + /** Remote Port to connect */ + remotePort?: number; + /** Remote socket path to connect */ + remoteSocketPath?: string; + socks?: boolean; + /** Unique name */ + name?: string; +} + +const defaultOptions: Partial = { + reconnect: false, + port: 22, + reconnectTries: 3, + reconnectDelay: 5000 +}; + +const SSHConstants = { + 'CHANNEL': { + SSH: 'ssh', + TUNNEL: 'tunnel', + X11: 'x11' + }, + 'STATUS': { + BEFORECONNECT: 'beforeconnect', + CONNECT: 'connect', + BEFOREDISCONNECT: 'beforedisconnect', + DISCONNECT: 'disconnect' + } +}; + +export default class SSHConnection extends EventEmitter { + public config: SSHConnectConfig; + + private activeTunnels: { [index: string]: SSHTunnelConfig & { server: Server } } = {}; + private __$connectPromise: Promise | null = null; + private __retries: number = 0; + private __err: Error & ClientErrorExtensions & { code?: string } | null = null; + private sshConnection: Client | null = null; + + constructor(options: SSHConnectConfig) { + super(); + this.config = Object.assign({}, defaultOptions, options); + this.config.uniqueId = this.config.uniqueId || `${this.config.username}@${this.config.host}`; + } + + /** + * Emit message on this channel + */ + override emit(channel: string, status: string, payload?: any): boolean { + super.emit(channel, status, this, payload); + return super.emit(`${channel}:${status}`, this, payload); + } + + /** + * Get shell socket + */ + shell(options: ShellOptions = {}): Promise { + return this.connect().then(() => { + return new Promise((resolve, reject) => { + this.sshConnection!.shell(options, (err, stream) => err ? reject(err) : resolve(stream)); + }); + }); + } + + /** + * Exec a command + */ + exec(cmd: string, params?: Array, options: ExecOptions = {}): Promise<{ stdout: string; stderr: string }> { + cmd += (Array.isArray(params) ? (' ' + params.join(' ')) : ''); + return this.connect().then(() => { + return new Promise((resolve, reject) => { + this.sshConnection!.exec(cmd, options, (err, stream) => { + if (err) { + return reject(err); + } + let stdout = ''; + let stderr = ''; + stream.on('close', function () { + return resolve({ stdout, stderr }); + }).on('data', function (data: Buffer | string) { + stdout += data.toString(); + }).stderr.on('data', function (data: Buffer | string) { + stderr += data.toString(); + }); + }); + }); + }); + } + + /** + * Exec a command + */ + execPartial(cmd: string, tester: (stdout: string, stderr: string) => boolean, params?: Array, options: ExecOptions = {}): Promise<{ stdout: string; stderr: string }> { + cmd += (Array.isArray(params) ? (' ' + params.join(' ')) : ''); + return this.connect().then(() => { + return new Promise((resolve, reject) => { + this.sshConnection!.exec(cmd, options, (err, stream) => { + if (err) { + return reject(err); + } + let stdout = ''; + let stderr = ''; + let resolved = false; + stream.on('close', function () { + if (!resolved) { + return resolve({ stdout, stderr }); + } + }).on('data', function (data: Buffer | string) { + stdout += data.toString(); + + if (tester(stdout, stderr)) { + resolved = true; + + return resolve({ stdout, stderr }); + } + }).stderr.on('data', function (data: Buffer | string) { + stderr += data.toString(); + + if (tester(stdout, stderr)) { + resolved = true; + + return resolve({ stdout, stderr }); + } + }); + }); + }); + }); + } + + /** + * Forward out + */ + forwardOut(srcIP: string, srcPort: number, destIP: string, destPort: number): Promise { + return this.connect().then(() => { + return new Promise((resolve, reject) => { + this.sshConnection!.forwardOut(srcIP, srcPort, destIP, destPort, (err, stream) => { + if (err) { + return reject(err); + } + resolve(stream); + }); + }); + }); + } + + /** + * Get a Socks Port + */ + getSocksPort(localPort: number): Promise { + return this.addTunnel({ name: '__socksServer', socks: true, localPort: localPort }).then((tunnel) => { + return tunnel.localPort!; + }); + } + + /** + * Close SSH Connection + */ + close(): Promise { + this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.BEFOREDISCONNECT); + return this.closeTunnel().then(() => { + if (this.sshConnection) { + this.sshConnection.end(); + this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.DISCONNECT); + } + }); + } + + /** + * Connect the SSH Connection + */ + connect(c?: SSHConnectConfig): Promise { + this.config = Object.assign(this.config, c); + ++this.__retries; + + if (this.__$connectPromise) { + return this.__$connectPromise; + } + + this.__$connectPromise = new Promise((resolve, reject) => { + this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.BEFORECONNECT); + if (!this.config || typeof this.config === 'function' || !(this.config.host || this.config.sock) || !this.config.username) { + reject(`Invalid SSH connection configuration host/username can't be empty`); + this.__$connectPromise = null; + return; + } + + if (this.config.identity) { + if (fs.existsSync(this.config.identity)) { + this.config.privateKey = fs.readFileSync(this.config.identity); + } + delete this.config.identity; + } + + //Start ssh server connection + this.sshConnection = new Client(); + this.sshConnection.on('ready', (err: Error & ClientErrorExtensions) => { + if (err) { + this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.DISCONNECT, { err: err }); + this.__$connectPromise = null; + return reject(err); + } + this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.CONNECT); + this.__retries = 0; + this.__err = null; + resolve(this); + }).on('error', (err) => { + this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.DISCONNECT, { err: err }); + this.__err = err; + }).on('close', () => { + this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.DISCONNECT, { err: this.__err }); + if (this.config.reconnect && this.__retries <= this.config.reconnectTries! && this.__err && this.__err.level !== 'client-authentication' && this.__err.code !== 'ENOTFOUND') { + setTimeout(() => { + this.__$connectPromise = null; + resolve(this.connect()); + }, this.config.reconnectDelay); + } else { + reject(this.__err); + } + }).connect(this.config); + }); + return this.__$connectPromise; + } + + /** + * Get existing tunnel by name + */ + getTunnel(name: string) { + return this.activeTunnels[name]; + } + + /** + * Add new tunnel if not exist + */ + addTunnel(SSHTunnelConfig: SSHTunnelConfig): Promise { + SSHTunnelConfig.name = SSHTunnelConfig.name || `${SSHTunnelConfig.remoteAddr}@${SSHTunnelConfig.remotePort || SSHTunnelConfig.remoteSocketPath}`; + this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.BEFORECONNECT, { SSHTunnelConfig: SSHTunnelConfig }); + if (this.getTunnel(SSHTunnelConfig.name)) { + this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.CONNECT, { SSHTunnelConfig: SSHTunnelConfig }); + return Promise.resolve(this.getTunnel(SSHTunnelConfig.name)); + } else { + return new Promise((resolve, reject) => { + let server: net.Server; + if (SSHTunnelConfig.socks) { + server = createSocksServer({ + connectionFilter: (destination: SocksConnectionInfo, origin: SocksConnectionInfo, callback: (err?: any, dest?: stream.Duplex) => void) => { + this.connect().then(() => { + this.sshConnection!.forwardOut( + origin.address, + origin.port, + destination.address, + destination.port, + (err, stream) => { + if (err) { + this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err }); + return callback(err); + } + return callback(null, stream); + }); + }); + } + }).on('proxyError', (err: any) => { + this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err }); + }); + } else { + server = net.createServer() + .on('connection', (socket) => { + this.connect().then(() => { + if (SSHTunnelConfig.remotePort) { + this.sshConnection!.forwardOut('127.0.0.1', 0, SSHTunnelConfig.remoteAddr!, SSHTunnelConfig.remotePort!, (err, stream) => { + if (err) { + this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err }); + return; + } + stream.pipe(socket); + socket.pipe(stream); + }); + } else { + this.sshConnection!.openssh_forwardOutStreamLocal(SSHTunnelConfig.remoteSocketPath!, (err, stream) => { + if (err) { + this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err }); + return; + } + stream.pipe(socket); + socket.pipe(stream); + }); + } + }); + }); + } + + SSHTunnelConfig.localPort = SSHTunnelConfig.localPort || 0; + server.on('listening', () => { + SSHTunnelConfig.localPort = (server.address() as net.AddressInfo).port; + this.activeTunnels[SSHTunnelConfig.name!] = Object.assign({}, { server }, SSHTunnelConfig); + this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.CONNECT, { SSHTunnelConfig: SSHTunnelConfig }); + resolve(this.activeTunnels[SSHTunnelConfig.name!]); + }).on('error', (err: any) => { + this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err }); + server.close(); + reject(err); + delete this.activeTunnels[SSHTunnelConfig.name!]; + }).listen(SSHTunnelConfig.localPort); + }); + } + } + + /** + * Close the tunnel + */ + closeTunnel(name?: string): Promise { + if (name && this.activeTunnels[name]) { + return new Promise((resolve) => { + const tunnel = this.activeTunnels[name]; + this.emit( + SSHConstants.CHANNEL.TUNNEL, + SSHConstants.STATUS.BEFOREDISCONNECT, + { SSHTunnelConfig: tunnel } + ); + tunnel.server.close(() => { + this.emit( + SSHConstants.CHANNEL.TUNNEL, + SSHConstants.STATUS.DISCONNECT, + { SSHTunnelConfig: this.activeTunnels[name] } + ); + delete this.activeTunnels[name]; + resolve(); + }); + }); + } else if (!name) { + const tunnels = Object.keys(this.activeTunnels).map((key) => this.closeTunnel(key)); + return Promise.all(tunnels).then(() => { }); + } + + return Promise.resolve(); + } +} diff --git a/extensions/open-remote-ssh/src/ssh/sshDestination.ts b/extensions/open-remote-ssh/src/ssh/sshDestination.ts new file mode 100644 index 00000000..db0b4406 --- /dev/null +++ b/extensions/open-remote-ssh/src/ssh/sshDestination.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export default class SSHDestination { + constructor( + public readonly hostname: string, + public readonly user?: string, + public readonly port?: number + ) { + } + + static parse(dest: string): SSHDestination { + let user: string | undefined; + const atPos = dest.lastIndexOf('@'); + if (atPos !== -1) { + user = dest.substring(0, atPos); + } + + let port: number | undefined; + const colonPos = dest.lastIndexOf(':'); + if (colonPos !== -1) { + port = parseInt(dest.substring(colonPos + 1), 10); + } + + const start = atPos !== -1 ? atPos + 1 : 0; + const end = colonPos !== -1 ? colonPos : dest.length; + const hostname = dest.substring(start, end); + + return new SSHDestination(hostname, user, port); + } + + toString(): string { + let result = this.hostname; + if (this.user) { + result = `${this.user}@` + result; + } + if (this.port) { + result = result + `:${this.port}`; + } + return result; + } + + // vscode.uri implementation lowercases the authority, so when reopen or restore + // a remote session from the recently openend list the connection fails + static parseEncoded(dest: string): SSHDestination { + try { + const data = JSON.parse(Buffer.from(dest, 'hex').toString()); + return new SSHDestination(data.hostName, data.user, data.port); + } catch { + } + return SSHDestination.parse(dest.replace(/\\x([0-9a-f]{2})/g, (_, charCode) => String.fromCharCode(parseInt(charCode, 16)))); + } + + toEncodedString(): string { + return this.toString().replace(/[A-Z]/g, (ch) => `\\x${ch.charCodeAt(0).toString(16).toLowerCase()}`); + } +} diff --git a/extensions/open-remote-ssh/tsconfig.json b/extensions/open-remote-ssh/tsconfig.json new file mode 100644 index 00000000..2e08e0d2 --- /dev/null +++ b/extensions/open-remote-ssh/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + }, + "include": [ + "src/**/*", + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.resolvers.d.ts", + "../../src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts", + ] +} From 7dcb08b09ab03c06c69429643c303cd72c36316d Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Feb 2025 20:50:04 -0800 Subject: [PATCH 02/92] comment out unwanted code --- .../void/browser/inlineDiffsService.ts | 322 +++++++++--------- .../contrib/void/browser/void.contribution.ts | 2 +- 2 files changed, 162 insertions(+), 162 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 998a286f..2be7ae7b 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags, fastApply_searchreplace_systemMessage, fastApply_searchreplace_userMessage } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags } from './prompt/prompts.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -1207,39 +1207,39 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { return null } - private _generateSearchAndReplaceBlocks({ filename, applyStr }: { filename: URI, applyStr: string }): DiffZone | undefined { + // private _generateSearchAndReplaceBlocks({ filename, applyStr }: { filename: URI, applyStr: string }): DiffZone | undefined { - // call LLM to generate search and replace blocks (outputs something like [{search: 'this is my code', replace: 'this is m'}, ... ]) + // // call LLM to generate search and replace blocks (outputs something like [{search: 'this is my code', replace: 'this is m'}, ... ]) - // 1a output search block + // // 1a output search block - let uri: URI + // let uri: URI - const uri_ = this._getActiveEditorURI() - if (!uri_) return - uri = uri_ + // const uri_ = this._getActiveEditorURI() + // if (!uri_) return + // uri = uri_ - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + // // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + // this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - // in ctrl+L the start and end lines are the full document + // // in ctrl+L the start and end lines are the full document - const numLines = this._getNumLines(uri) - if (numLines === null) return + // const numLines = this._getNumLines(uri) + // if (numLines === null) return - let startLine: number - let endLine: number + // let startLine: number + // let endLine: number - startLine = 1 - endLine = numLines + // startLine = 1 + // endLine = numLines - const currentFileStr = this._readURI(uri) - if (currentFileStr === null) return - const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + // const currentFileStr = this._readURI(uri) + // if (currentFileStr === null) return + // const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - // 1b find the start and end line that the search block lives on (if can't find it, retry 1a) + // // 1b find the start and end line that the search block lives on (if can't find it, retry 1a) @@ -1247,174 +1247,174 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { - let streamRequestIdRef: { current: string | null } = { current: null } + // let streamRequestIdRef: { current: string | null } = { current: null } - // add to history - const { onFinishEdit } = this._addToHistory(uri) + // // add to history + // const { onFinishEdit } = this._addToHistory(uri) - // __TODO__ let users customize modelFimTags - const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama' - const modelFimTags = defaultFimTags + // // __TODO__ let users customize modelFimTags + // const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama' + // const modelFimTags = defaultFimTags - const adding: Omit = { - type: 'DiffZone', - originalCode, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), - } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) + // const adding: Omit = { + // type: 'DiffZone', + // originalCode, + // startLine, + // endLine, + // _URI: uri, + // _streamState: { + // isStreaming: true, + // streamRequestIdRef, + // line: startLine, + // }, + // _diffOfId: {}, // added later + // _removeStylesFns: new Set(), + // } + // const diffZone = this._addDiffArea(adding) + // this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + // this._onDidAddOrDeleteDiffZones.fire({ uri }) - if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return + // if (from === 'QuickEdit') { + // const { diffareaid } = opts + // const ctrlKZone = this.diffAreaOfId[diffareaid] + // if (ctrlKZone.type !== 'CtrlKZone') return - ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid - } + // ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + // } - // now handle messages - let messages: LLMChatMessage[] + // // now handle messages + // let messages: LLMChatMessage[] - if (from === 'Chat') { - const userContent = fastApply_searchreplace_userMessage({ originalCode, applyStr: opts.applyStr, uri }) - messages = [ - { role: 'system', content: fastApply_rewritewholething_systemMessage, }, - { role: 'user', content: userContent, } - ] - } - else if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - const { _mountInfo } = ctrlKZone - const instructions = _mountInfo?.textAreaRef.current?.value ?? '' + // if (from === 'Chat') { + // const userContent = fastApply_searchreplace_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + // messages = [ + // { role: 'system', content: fastApply_rewritewholething_systemMessage, }, + // { role: 'user', content: userContent, } + // ] + // } + // else if (from === 'QuickEdit') { + // const { diffareaid } = opts + // const ctrlKZone = this.diffAreaOfId[diffareaid] + // if (ctrlKZone.type !== 'CtrlKZone') return + // const { _mountInfo } = ctrlKZone + // const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else: - const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - // if (isOllamaFIM) { - // messages = { - // type: 'ollamaFIM', - // prefix, - // suffix, - // } + // // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else: + // const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) + // // if (isOllamaFIM) { + // // messages = { + // // type: 'ollamaFIM', + // // prefix, + // // suffix, + // // } - // } - // else { - const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) - // type: 'messages', - messages = [ - { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, - { role: 'user', content: userContent, } - ] - // } - } - else { throw new Error(`featureName ${from} is invalid`) } + // // } + // // else { + // const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + // const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) + // // type: 'messages', + // messages = [ + // { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, + // { role: 'user', content: userContent, } + // ] + // // } + // } + // else { throw new Error(`featureName ${from} is invalid`) } - const onDone = (hadError: boolean) => { - diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + // const onDone = (hadError: boolean) => { + // diffZone._streamState = { isStreaming: false, } + // this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - if (from === 'QuickEdit') { - const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone + // if (from === 'QuickEdit') { + // const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone - ctrlKZone._linkedStreamingDiffZone = null - this._deleteCtrlKZone(ctrlKZone) - } - this._refreshStylesAndDiffsInURI(uri) - onFinishEdit() + // ctrlKZone._linkedStreamingDiffZone = null + // this._deleteCtrlKZone(ctrlKZone) + // } + // this._refreshStylesAndDiffsInURI(uri) + // onFinishEdit() - // if had error, revert! - if (hadError) { - this._undoHistory(diffZone._URI) - } - } + // // if had error, revert! + // if (hadError) { + // this._undoHistory(diffZone._URI) + // } + // } - // refresh now in case onText takes a while to get 1st message - this._refreshStylesAndDiffsInURI(uri) + // // refresh now in case onText takes a while to get 1st message + // this._refreshStylesAndDiffsInURI(uri) - const extractText = (fullText: string, recentlyAddedTextLen: number) => { - if (from === 'QuickEdit') { - if (isOllamaFIM) return fullText - return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) - } - else if (from === 'Chat') { - return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) - } - throw 1 - } + // const extractText = (fullText: string, recentlyAddedTextLen: number) => { + // if (from === 'QuickEdit') { + // if (isOllamaFIM) return fullText + // return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) + // } + // else if (from === 'Chat') { + // return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) + // } + // throw 1 + // } - const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + // const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - // state used in onText: - let fullText = '' - let prevIgnoredSuffix = '' + // // state used in onText: + // let fullText = '' + // let prevIgnoredSuffix = '' - streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K', - logging: { loggingName: `startApplying - ${from}` }, - messages, - onText: ({ newText: newText_ }) => { + // streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + // messagesType: 'chatMessages', + // useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K', + // logging: { loggingName: `startApplying - ${from}` }, + // messages, + // onText: ({ newText: newText_ }) => { - const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! - fullText += prevIgnoredSuffix + newText + // const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! + // fullText += prevIgnoredSuffix + newText - const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) - this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) - this._refreshStylesAndDiffsInURI(uri) + // const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) + // this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) + // this._refreshStylesAndDiffsInURI(uri) - prevIgnoredSuffix = ignoredSuffix - }, - onFinalMessage: ({ fullText }) => { - // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) - // at the end, re-write whole thing to make sure no sync errors - const [text, _] = extractText(fullText, 0) - this._writeText(uri, text, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - onDone(false) - }, - onError: (e) => { - const details = errorDetails(e.fullError) - this._notificationService.notify({ - severity: Severity.Warning, - message: `Void Error: ${e.message}`, - actions: { - secondary: [{ - id: 'void.onerror.opensettings', - enabled: true, - label: 'Open Void settings', - tooltip: '', - class: undefined, - run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } - }] - }, - source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined - }) - onDone(true) - }, + // prevIgnoredSuffix = ignoredSuffix + // }, + // onFinalMessage: ({ fullText }) => { + // // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) + // // at the end, re-write whole thing to make sure no sync errors + // const [text, _] = extractText(fullText, 0) + // this._writeText(uri, text, + // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + // { shouldRealignDiffAreas: true } + // ) + // onDone(false) + // }, + // onError: (e) => { + // const details = errorDetails(e.fullError) + // this._notificationService.notify({ + // severity: Severity.Warning, + // message: `Void Error: ${e.message}`, + // actions: { + // secondary: [{ + // id: 'void.onerror.opensettings', + // enabled: true, + // label: 'Open Void settings', + // tooltip: '', + // class: undefined, + // run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } + // }] + // }, + // source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined + // }) + // onDone(true) + // }, - }) + // }) - return diffZone + // return diffZone - } + // } diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 19d20201..c10ca414 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -22,7 +22,7 @@ import './chatThreadService.js' import './autocompleteService.js' // register Context services -import './contextGatheringService.js' +// import './contextGatheringService.js' // import './contextUserChangesService.js' // settings pane From 4f9f16b93f31c580027b30a09a0e67cd235ca707 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Feb 2025 21:27:39 -0800 Subject: [PATCH 03/92] almost works (but doesn't delete old text) --- .../browser/helpers/extractCodeFromResult.ts | 74 +++++++++ .../void/browser/inlineDiffsService.ts | 153 +++++++----------- .../react/src/markdown/ChatMarkdownRender.tsx | 2 +- 3 files changed, 133 insertions(+), 96 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 1cb53e5c..a0addcae 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -175,3 +175,77 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te } + + + +export type ExtractedSearchReplaceBlock = { + state: 'writingOriginal' | 'writingFinal' | 'done', + orig: string, + final: string, +} + + +const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => { + // for each prefix + for (let i = anyPrefix.length; i >= 0; i--) { + const prefix = anyPrefix.slice(0, i) + if (str.endsWith(prefix)) return prefix + } + return null +} + +// guarantees if you keep adding text, array length will strictly grow and state will progress without going back +export const extractSearchReplaceBlocks = (str: string, { ORIGINAL, DIVIDER, FINAL }: { ORIGINAL: string, DIVIDER: string, FINAL: string }) => { + + const ORIGINAL_ = ORIGINAL + `\n` + const DIVIDER_ = '\n' + DIVIDER + `\n` + const FINAL_ = '\n' + FINAL + + + const blocks: ExtractedSearchReplaceBlock[] = [] + + let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way) + while (true) { + let origStart = str.indexOf(ORIGINAL_, i) + if (origStart === -1) { return blocks } + origStart += ORIGINAL_.length + i = origStart + // wrote <<<< ORIGINAL + + let dividerStart = str.indexOf(DIVIDER_, i) + if (dividerStart === -1) { // if didnt find DIVIDER_, either writing originalStr or DIVIDER_ right now + const isWritingDIVIDER = endsWithAnyPrefixOf(str, DIVIDER_) + blocks.push({ + orig: str.substring(origStart, str.length - (isWritingDIVIDER?.length ?? 0)), + final: '', + state: 'writingOriginal' + }) + return blocks + } + const origStrDone = str.substring(origStart, dividerStart) + dividerStart += DIVIDER_.length + i = dividerStart + // wrote ===== + + let finalStart = str.indexOf(FINAL_, i) + if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now + const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_) + blocks.push({ + orig: origStrDone, + final: str.substring(dividerStart, str.length - (isWritingFINAL?.length ?? 0)), + state: 'writingFinal' + }) + return blocks + } + const finalStrDone = str.substring(dividerStart, finalStart) + finalStart += FINAL_.length + i = finalStart + // wrote >>>>> FINAL + + blocks.push({ + orig: origStrDone, + final: finalStrDone, + state: 'done' + }) + } +} diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index ed435a26..81e5dcbc 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -30,7 +30,7 @@ import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; -import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from './helpers/extractCodeFromResult.js'; import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; @@ -138,12 +138,6 @@ export type Diff = { -type ExtractedCodeBlock = { - state: 'writingOriginal' | 'writingFinal' | 'done', - orig: string, - final: string, -} - // _ means anything we don't include if we clone it // DiffArea.originalStartLine is the line in originalCode (not the file) @@ -1249,71 +1243,6 @@ INSTRUCTIONS Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation. ` - const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => { - // for each prefix - for (let i = anyPrefix.length; i >= 0; i--) { - const prefix = anyPrefix.slice(0, i) - if (str.endsWith(prefix)) return prefix - } - return null - } - - const extractBlocks = (str: string) => { - - const ORIGINAL_ = ORIGINAL + `\n` - const DIVIDER_ = '\n' + DIVIDER + `\n` - const FINAL_ = '\n' + FINAL - - - const blocks: ExtractedCodeBlock[] = [] - - let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way) - while (true) { - let origStart = str.indexOf(ORIGINAL_, i) - if (origStart === -1) { return blocks } - origStart += ORIGINAL_.length - i = origStart - // wrote <<<< ORIGINAL - - let dividerStart = str.indexOf(DIVIDER_, i) - if (dividerStart === -1) { // if didnt find DIVIDER_, either writing originalStr or DIVIDER_ right now - const isWritingDIVIDER = endsWithAnyPrefixOf(str, DIVIDER_) - blocks.push({ - orig: str.substring(origStart, str.length - (isWritingDIVIDER?.length ?? 0)), - final: '', - state: 'writingOriginal' - }) - return blocks - } - const origStrDone = str.substring(origStart, dividerStart) - dividerStart += DIVIDER_.length - i = dividerStart - // wrote ===== - - let finalStart = str.indexOf(FINAL_, i) - if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now - const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_) - blocks.push({ - orig: origStrDone, - final: str.substring(origStart, str.length - (isWritingFINAL?.length ?? 0)), - state: 'writingFinal' - }) - return blocks - } - const finalStrDone = str.substring(dividerStart, finalStart) - finalStart += FINAL_.length - i = finalStart - // wrote >>>>> FINAL - - blocks.push({ - orig: origStrDone, - final: finalStrDone, - state: 'done' - }) - } - } - - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) @@ -1326,8 +1255,16 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest const diffareaidOfBlockNum: number[] = [] + // TODO replace all these with whatever block we're on initially if already started + let latestStreamInfoMutable: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number } | null = null + let currStreamingBlockNum = 0 + let oldBlocks: ExtractedSearchReplaceBlock[] = [] + const onText = ({ newText, fullText }: { newText: string, fullText: string }) => { - const blocks = extractBlocks(fullText) + console.log('FULLTEXT', fullText) + console.log('NEW', newText) + + const blocks = extractSearchReplaceBlocks(fullText, { ORIGINAL, DIVIDER, FINAL }) // find block.orig in fileContents and return its range in file const findTextInCode = (text: string, fileContents: string) => { @@ -1341,22 +1278,27 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest return [startLine, endLine] } - let latestStreamInfoMutable: any = {} - for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { + for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] - if (block.state === 'writingOriginal') continue - const foundInCode = findTextInCode(block.orig, fileContents) - if (typeof foundInCode === 'string') { - console.log('ERROR!!!!', foundInCode) + if (block.state === 'done') + currStreamingBlockNum = blockNum + + if (block.state === 'writingOriginal') continue - } - - const [startLine, endLine] = foundInCode + let deltaFinalText: string // if should add new diffarea - if (blockNum > diffareaidOfBlockNum.length) { + if (!(blockNum in diffareaidOfBlockNum)) { + const foundInCode = findTextInCode(block.orig, fileContents) + if (typeof foundInCode === 'string') { + console.log('NOT FOUND IN CODE!!!!', foundInCode) + break + } + const [startLine, endLine] = foundInCode + + console.log('ADDING', blockNum) const adding: Omit = { type: 'DiffZone', originalCode: block.orig, @@ -1378,19 +1320,30 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest diffareaidOfBlockNum.push(diffZone.diffareaid) latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + + deltaFinalText = block.final + } + else { + deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) } + console.log('DELTA', deltaFinalText) + oldBlocks = blocks + + // write new text to diffarea const diffareaid = diffareaidOfBlockNum[blockNum] const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone.type !== 'DiffZone') continue + if (diffZone?.type !== 'DiffZone') continue - this._writeStreamedDiffZoneLLMText(diffZone, fullText, newText, latestStreamInfoMutable) - this._refreshStylesAndDiffsInURI(uri) + + if (!latestStreamInfoMutable) continue + this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamInfoMutable) } - + this._refreshStylesAndDiffsInURI(uri) } + const { onFinishEdit } = this._addToHistory(uri) // TODO turn this into a service and provide it @@ -1400,8 +1353,18 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest logging: { loggingName: `generateSearchAndReplace` }, messages, onText: ({ newText, fullText }) => { onText({ newText, fullText }) }, - onFinalMessage: ({ fullText }) => { }, - onError: (e) => { console.log('ERROR', e) }, + onFinalMessage: ({ fullText }) => { + // 1. wait 500ms and fix lint errors - call lint error workflow + // (update react state to say "Fixing errors") + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + + }, + onError: (e) => { + console.log('ERROR', e); + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + }, }) @@ -1564,19 +1527,19 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest onText: ({ newText: newText_ }) => { const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! - fullText += prevIgnoredSuffix + newText + fullText += prevIgnoredSuffix + newText // full text, including ```, etc - const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) - this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfoMutable) + const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) + this._writeStreamedDiffZoneLLMText(diffZone, croppedText, deltaCroppedText, latestStreamInfoMutable) this._refreshStylesAndDiffsInURI(uri) - prevIgnoredSuffix = ignoredSuffix + prevIgnoredSuffix = croppedSuffix }, onFinalMessage: ({ fullText }) => { // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) // at the end, re-write whole thing to make sure no sync errors - const [text, _] = extractText(fullText, 0) - this._writeText(uri, text, + const [croppedText, _1, _2] = extractText(fullText, 0) + this._writeText(uri, croppedText, { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 7b03f068..c6f88762 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -102,7 +102,7 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati // deal with built-in tokens first (assume marked token) const t = token as MarkedToken - console.log(t.raw) + // console.log('render:', t.raw) if (t.type === "space") { return {t.raw} From 7cdb003c47a9ec50e06160c0255f9a3539e5ee08 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Feb 2025 21:51:58 -0800 Subject: [PATCH 04/92] stream state --- .../void/browser/inlineDiffsService.ts | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 81e5dcbc..78a02e40 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -211,6 +211,10 @@ type HistorySnapshot = { +// line/col is the location, originalCodeStartLine is the start line of the original code being displayed +type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number } + + export interface IInlineDiffsService { readonly _serviceBrand: undefined; startApplying(opts: StartApplyingOpts): number | undefined; @@ -996,7 +1000,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // @throttle(100) - private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latestMutable: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { + private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latestMutable: StreamLocationMutable) { // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out @@ -1256,13 +1260,11 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest const diffareaidOfBlockNum: number[] = [] // TODO replace all these with whatever block we're on initially if already started - let latestStreamInfoMutable: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number } | null = null + let latestStreamLocationMutable: StreamLocationMutable | null = null let currStreamingBlockNum = 0 let oldBlocks: ExtractedSearchReplaceBlock[] = [] const onText = ({ newText, fullText }: { newText: string, fullText: string }) => { - console.log('FULLTEXT', fullText) - console.log('NEW', newText) const blocks = extractSearchReplaceBlocks(fullText, { ORIGINAL, DIVIDER, FINAL }) @@ -1297,6 +1299,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest break } const [startLine, endLine] = foundInCode + console.log('FOUND!', foundInCode) console.log('ADDING', blockNum) const adding: Omit = { @@ -1319,7 +1322,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest diffareaidOfBlockNum.push(diffZone.diffareaid) - latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + latestStreamLocationMutable = { line: diffZone.endLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } deltaFinalText = block.final } @@ -1327,7 +1330,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) } - console.log('DELTA', deltaFinalText) + console.log('FULLTEXT', block.final) oldBlocks = blocks // write new text to diffarea @@ -1336,13 +1339,34 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest if (diffZone?.type !== 'DiffZone') continue - if (!latestStreamInfoMutable) continue - this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamInfoMutable) - } + if (!latestStreamLocationMutable) continue + this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) + } // end for + this._refreshStylesAndDiffsInURI(uri) } + + const onDone = (hadError: boolean) => { + for (const blockNum in diffareaidOfBlockNum) { + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') continue + + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + + // if had error, revert! + if (hadError) this._undoHistory(diffZone._URI) + } + } + + + const { onFinishEdit } = this._addToHistory(uri) @@ -1352,18 +1376,18 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest useProviderFor: 'FastApply', logging: { loggingName: `generateSearchAndReplace` }, messages, - onText: ({ newText, fullText }) => { onText({ newText, fullText }) }, + onText: ({ newText, fullText }) => { + onText({ newText, fullText }) + }, onFinalMessage: ({ fullText }) => { // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") - this._refreshStylesAndDiffsInURI(uri) - onFinishEdit() + onDone(false) }, onError: (e) => { console.log('ERROR', e); - this._refreshStylesAndDiffsInURI(uri) - onFinishEdit() + onDone(true) }, }) @@ -1513,7 +1537,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest throw 1 } - const latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } // state used in onText: let fullText = '' From a479a32ad5ab0382180d6d3b8906c9db7312c237 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Feb 2025 21:55:58 -0800 Subject: [PATCH 05/92] small fix --- .../contrib/void/browser/inlineDiffsService.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 78a02e40..1ee8a8a3 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -1287,7 +1287,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest if (block.state === 'done') currStreamingBlockNum = blockNum - if (block.state === 'writingOriginal') + if (block.state === 'writingOriginal') // must be done writing original continue let deltaFinalText: string @@ -1353,16 +1353,12 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest const diffareaid = diffareaidOfBlockNum[blockNum] const diffZone = this.diffAreaOfId[diffareaid] if (diffZone?.type !== 'DiffZone') continue - diffZone._streamState = { isStreaming: false, } this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - - this._refreshStylesAndDiffsInURI(uri) - onFinishEdit() - - // if had error, revert! - if (hadError) this._undoHistory(diffZone._URI) } + this._refreshStylesAndDiffsInURI(uri) + if (hadError) this._undoHistory(uri) + onFinishEdit() } From f7af9c336b09d3bbb812acc1ead6a59ff4fbeb90 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Feb 2025 21:58:57 -0800 Subject: [PATCH 06/92] tokenIdx --- .../browser/react/src/markdown/ChatMarkdownRender.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index c6f88762..8f569074 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -21,7 +21,7 @@ const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' -type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: number } +type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { return `${threadId}-${messageIdx}-${tokenIdx}` @@ -97,7 +97,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c } -const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation: chatLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: number }): JSX.Element => { +const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation: chatLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { // deal with built-in tokens first (assume marked token) @@ -206,7 +206,7 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati if (t.type === "paragraph") { const contents = <> {t.tokens.map((token, index) => ( - // assign a unique tokenId to nested components + // assign a unique tokenId to nested components ))} if (nested) return contents @@ -294,7 +294,7 @@ export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessag return ( <> {tokens.map((token, index) => ( - + ))} ) From 34b8027fb944fd7330aaaa4e4a102d627f8e06e2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 13 Feb 2025 00:08:23 -0800 Subject: [PATCH 07/92] streaming progress --- .../void/browser/inlineDiffsService.ts | 173 +++++++++--------- 1 file changed, 88 insertions(+), 85 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 1ee8a8a3..38a1a46f 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -1264,88 +1264,19 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest let currStreamingBlockNum = 0 let oldBlocks: ExtractedSearchReplaceBlock[] = [] - const onText = ({ newText, fullText }: { newText: string, fullText: string }) => { - - const blocks = extractSearchReplaceBlocks(fullText, { ORIGINAL, DIVIDER, FINAL }) - - // find block.orig in fileContents and return its range in file - const findTextInCode = (text: string, fileContents: string) => { - const idx = fileContents.indexOf(text) - if (idx === -1) return 'Not found' as const - const lastIdx = fileContents.lastIndexOf(text) - if (lastIdx !== idx) return 'Not unique' as const - const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = text.split('\n').length - const endLine = startLine + numLines - 1 - return [startLine, endLine] - } - - - for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] - - if (block.state === 'done') - currStreamingBlockNum = blockNum - - if (block.state === 'writingOriginal') // must be done writing original - continue - - let deltaFinalText: string - // if should add new diffarea - if (!(blockNum in diffareaidOfBlockNum)) { - const foundInCode = findTextInCode(block.orig, fileContents) - if (typeof foundInCode === 'string') { - console.log('NOT FOUND IN CODE!!!!', foundInCode) - break - } - const [startLine, endLine] = foundInCode - console.log('FOUND!', foundInCode) - - console.log('ADDING', blockNum) - const adding: Omit = { - type: 'DiffZone', - originalCode: block.orig, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), - } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) - - diffareaidOfBlockNum.push(diffZone.diffareaid) - - latestStreamLocationMutable = { line: diffZone.endLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - - deltaFinalText = block.final - } - else { - deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) - } - - console.log('FULLTEXT', block.final) - oldBlocks = blocks - - // write new text to diffarea - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue - - - if (!latestStreamLocationMutable) continue - this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) - } // end for - - this._refreshStylesAndDiffsInURI(uri) + // find block.orig in fileContents and return its range in file + const findTextInCode = (text: string, fileContents: string) => { + const idx = fileContents.indexOf(text) + if (idx === -1) return 'Not found' as const + const lastIdx = fileContents.lastIndexOf(text) + if (lastIdx !== idx) return 'Not unique' as const + const startLine = fileContents.substring(0, idx).split('\n').length + const numLines = text.split('\n').length + const endLine = startLine + numLines - 1 + return [startLine, endLine] } + const { onFinishEdit } = this._addToHistory(uri) const onDone = (hadError: boolean) => { @@ -1363,7 +1294,6 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest - const { onFinishEdit } = this._addToHistory(uri) // TODO turn this into a service and provide it @@ -1372,14 +1302,87 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest useProviderFor: 'FastApply', logging: { loggingName: `generateSearchAndReplace` }, messages, - onText: ({ newText, fullText }) => { - onText({ newText, fullText }) + onText: ({ fullText }) => { + const blocks = extractSearchReplaceBlocks(fullText, { ORIGINAL, DIVIDER, FINAL }) + + for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + + if (block.state === 'done') + currStreamingBlockNum = blockNum + + if (block.state === 'writingOriginal') // must be done writing original + continue + + // if should add new diffarea + if (!(blockNum in diffareaidOfBlockNum)) { + const foundInCode = findTextInCode(block.orig, fileContents) + if (typeof foundInCode === 'string') { + console.log('NOT FOUND IN CODE!!!!', foundInCode) + continue + } + const [startLine, endLine] = foundInCode + console.log('FOUND!', foundInCode) + + console.log('ADDING', blockNum) + const adding: Omit = { + type: 'DiffZone', + originalCode: block.orig, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), + } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) + + diffareaidOfBlockNum.push(diffZone.diffareaid) + + latestStreamLocationMutable = { line: diffZone.endLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + + + } + const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) + oldBlocks = blocks + + // write new text to diffarea + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') continue + + + if (!latestStreamLocationMutable) continue + this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) + } // end for + + this._refreshStylesAndDiffsInURI(uri) }, - onFinalMessage: ({ fullText }) => { + onFinalMessage: async ({ fullText }) => { + // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") - onDone(false) + const blocks = extractSearchReplaceBlocks(fullText, { ORIGINAL, DIVIDER, FINAL }) + console.log('FULLTEXT', fullText, blocks) + for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') continue + + await this._writeText(uri, block.final, + { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + } + onDone(false) }, onError: (e) => { console.log('ERROR', e); From b01684393a122886374ee6c1aea0f739fb729d9b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 13 Feb 2025 00:37:43 -0800 Subject: [PATCH 08/92] streaming works! --- .../contrib/void/browser/inlineDiffsService.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 38a1a46f..ead98264 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -700,7 +700,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } weAreWriting = false - private async _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { + private _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { const model = this._getModel(uri) if (!model) return const uriStr = this._readURI(uri, range) @@ -1322,9 +1322,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest continue } const [startLine, endLine] = foundInCode - console.log('FOUND!', foundInCode) - console.log('ADDING', blockNum) const adding: Omit = { type: 'DiffZone', originalCode: block.orig, @@ -1345,8 +1343,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest diffareaidOfBlockNum.push(diffZone.diffareaid) - latestStreamLocationMutable = { line: diffZone.endLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - + latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) @@ -1365,11 +1362,9 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { - // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") const blocks = extractSearchReplaceBlocks(fullText, { ORIGINAL, DIVIDER, FINAL }) - console.log('FULLTEXT', fullText, blocks) for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] @@ -1377,7 +1372,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest const diffZone = this.diffAreaOfId[diffareaid] if (diffZone?.type !== 'DiffZone') continue - await this._writeText(uri, block.final, + this._writeText(uri, block.final, { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) From 28bf838b55c1a4e462659ccbfddfe0b67c458511 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina <75667013+bjoaquinc@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:05:30 +0700 Subject: [PATCH 09/92] Updated void-server endpoiont to make it dynamic --- extensions/open-remote-ssh/src/serverSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/open-remote-ssh/src/serverSetup.ts b/extensions/open-remote-ssh/src/serverSetup.ts index d851988a..d74ff8e9 100644 --- a/extensions/open-remote-ssh/src/serverSetup.ts +++ b/extensions/open-remote-ssh/src/serverSetup.ts @@ -39,7 +39,7 @@ export class ServerInstallError extends Error { } } -const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/${NAME_OF_REPO}/releases/download/${version}.${release}/void-server-${os}-${arch}-${version}.${release}.tar.gz'; +const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/void-updates-server/releases/download/test/void-server-${os}-${arch}.tar.gz'; export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise { let shell = 'powershell'; From d9e4679b654cb7fde5b1c4eea0ed817c639d1afe Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 13 Feb 2025 22:58:42 -0800 Subject: [PATCH 10/92] breaking changes - ai regex --- .../contrib/void/browser/aiRegexService.ts | 187 ++++++++++++++++++ .../browser/helpers/extractCodeFromResult.ts | 4 +- .../void/browser/inlineDiffsService.ts | 71 +------ .../contrib/void/browser/prompt/prompts.ts | 126 ++++++++++++ .../void/browser/searchAndReplaceService.ts | 71 ------- .../contrib/void/common/toolsService.ts | 4 +- 6 files changed, 324 insertions(+), 139 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/aiRegexService.ts delete mode 100644 src/vs/workbench/contrib/void/browser/searchAndReplaceService.ts diff --git a/src/vs/workbench/contrib/void/browser/aiRegexService.ts b/src/vs/workbench/contrib/void/browser/aiRegexService.ts new file mode 100644 index 00000000..9f96da76 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/aiRegexService.ts @@ -0,0 +1,187 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IToolService, ToolService } from '../common/toolsService.js'; + + + +export type ChatMessageLocation = { + threadId: string; + messageIdx: number; +} + + +export type SearchAndReplaceBlock = { + search: string; + replace: string; +} + +// service that manages state +export type ApplyState = { + [applyBoxId: string]: { + searchAndReplaceBlocks: SearchAndReplaceBlock; + } +} + +// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion` + +export interface IFastApplyService { + readonly _serviceBrand: undefined; + + // readonly state: ApplyState; // readonly to the user + // setState(newState: Partial): void; + // onDidChangeState: Event; +} + +export const IVoidFastApplyService = createDecorator('voidFastApplyService'); +class VoidFastApplyService extends Disposable implements IFastApplyService { + _serviceBrand: undefined; + + static readonly ID = 'voidFastApplyService'; + + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; + + + // state + // state: ApplyState + + constructor( + @IToolService private readonly toolService: ToolService + ) { + super() + + // initial state + // this.state = { currentUri: undefined } + } + + setState(newState: Partial) { + + // this.state = { ...this.state, ...newState } + this._onDidChangeState.fire() + } + + aiSearch(searchStr: string) { + + } + + aiReplace(searchStr: string, replaceStr: string) { + + } + + + // 1. search(ai) + // - tool use to find all possible changes + // - if search only: is this file related to the search? + // - if search + replace: should I modify this file? + // 2. replace(ai) + // - what changes to make? + // 3. postprocess errors + // -fastapply changes simultaneously + // -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error) + + + private async _searchUsingAI({ searchClause }: { searchClause: string }) { + + const relevantURIs: URI[] = [] + const gatherPrompt = `\ +asdasdas +` + const filterPrompt = `\ +Is this file relevant? +` + + + // optimizations (DO THESE LATER!!!!!!) + // if tool includes a uri in uriSet, skip it obviously + let uriSet = new Set() + // gather + let messages = [] + while (true) { + const result = await new Promise((res, rej) => { + sendLLMMessage({ + messages, + tools: ['search'], + onFinalMessage: ({ result: r, }) => { + res(r) + }, + onError: (error) => { + rej(error) + } + }) + }) + + messages.push({ role: 'tool', content: turnToString(result) }) + + sendLLMMessage({ + messages: { 'Output ': result }, + onFinalMessage: (r) => { + // output is file1\nfile2\nfile3\n... + } + }) + + uriSet.add(...) + } + + // writes + if (!replaceClause) return + + for (const uri of uriSet) { + // in future, batch these + applyWorkflow({ uri, applyStr: replaceClause }) + } + + + + + + + // while (true) { + // const result = new Promise((res, rej) => { + // sendLLMMessage({ + // messages, + // tools: ['search'], + // onResult: (r) => { + // res(r) + // } + // }) + // }) + + // messages.push(result) + + // } + + + } + + + private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) { + + for (const uri of relevantURIs) { + + uri + + } + + + + // should I change this file? + // if so what changes to make? + + + + // fast apply the changes + } + + + +} + +registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index a0addcae..bb1ed350 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -3,6 +3,8 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts' + class SurroundingsRemover { readonly originalS: string i: number @@ -195,7 +197,7 @@ const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => { } // guarantees if you keep adding text, array length will strictly grow and state will progress without going back -export const extractSearchReplaceBlocks = (str: string, { ORIGINAL, DIVIDER, FINAL }: { ORIGINAL: string, DIVIDER: string, FINAL: string }) => { +export const extractSearchReplaceBlocks = (str: string) => { const ORIGINAL_ = ORIGINAL + `\n` const DIVIDER_ = '\n' + DIVIDER + `\n` diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index ead98264..ed436b61 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, tripleTick } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -1173,59 +1173,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { - const ORIGINAL = `<<<<<<< ORIGINAL` - const DIVIDER = `=======` - const FINAL = `>>>>>>> UPDATED` - const searchReplaceSysMessage = `\ -You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file. - -A SEARCH/REPLACE block describes the code before and after a change. Here is the format: -${ORIGINAL} -// ... original code goes here -${DIVIDER} -// ... final code goes here -${FINAL} - -You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make. -Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks. - -Directions: -1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. -2. The "original" code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. -3. The "original" code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. -4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. - - Make sure you add all necessary imports. - - Make sure the "final" code is complete and will not result in syntax/lint errors. -5. Follow coding convention (spaces, semilcolons, comments, etc). - -## EXAMPLE 1 -ORIGINAL_FILE -${tripleTick[0]} -let w = 5 -let x = 6 -let y = 7 -let z = 8 -${tripleTick[1]} - -CHANGE -Make x equal to 6.5, not 6. -${tripleTick[0]} -// ... existing code -let x = 6.5 -// ... existing code -${tripleTick[1]} - - -## ACCEPTED OUTPUT -${tripleTick[0]} -${ORIGINAL} -let x = 6 -${DIVIDER} -let x = 6.5 -${FINAL} -${tripleTick[1]} -` const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1236,23 +1184,14 @@ ${tripleTick[1]} if (fileContents === null) return - const searchReplaceUserMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ -ORIGINAL_FILE -${originalCode} -CHANGE -${applyStr} - -INSTRUCTIONS -Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation. -` // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - const userMessageContent = searchReplaceUserMessage({ originalCode: fileContents, applyStr: applyStr }) + const userMessageContent = searchReplace_userMessage({ originalCode: fileContents, applyStr: applyStr }) const messages: LLMChatMessage[] = [ - { role: 'system', content: searchReplaceSysMessage }, + { role: 'system', content: searchReplace_systemMessage }, { role: 'user', content: userMessageContent } ] let streamRequestIdRef: { current: string | null } = { current: null } @@ -1303,7 +1242,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest logging: { loggingName: `generateSearchAndReplace` }, messages, onText: ({ fullText }) => { - const blocks = extractSearchReplaceBlocks(fullText, { ORIGINAL, DIVIDER, FINAL }) + const blocks = extractSearchReplaceBlocks(fullText) for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] @@ -1364,7 +1303,7 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest onFinalMessage: async ({ fullText }) => { // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") - const blocks = extractSearchReplaceBlocks(fullText, { ORIGINAL, DIVIDER, FINAL }) + const blocks = extractSearchReplaceBlocks(fullText) for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 428625fd..9185e0dc 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -224,7 +224,133 @@ Please finish writing the new file by applying the change to the original file. +const aiRegex_computeReplacementsForFile_systemMessage = `\ +You are a "search and replace" coding assistant. +You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE. + +The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for. + +The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace. + +The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes. + +## Instructions + +1. If you do not want to make any changes, you should respond with the word "no". + +2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make. +For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name. + - Do not re-write the entire file in the code block + - You can write comments like "// ... existing code" to indicate existing code + - Make sure you give enough context in the code block to apply the changes to the correct location in the code` + + +const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService }) => { + + // we may want to do this in batches + const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null } + + const file = await stringifyFileSelections([fileSelection], modelService) + + return `\ +## FILE +${file} + +## SEARCH_CLAUSE +Here is what the user is searching for: +${searchClause} + +## REPLACE_CLAUSE +Here is what the user wants to replace it with: +${replaceClause} + +## INSTRUCTIONS +Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.` +} + + + + +// don't have to tell it it will be given the history; just give it to it +const aiRegex_search_systemMessage = `\ +You are a coding assistant that executes the SEARCH part of a user's search and replace query. + +You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context. + +Output +- Regex query +- Files to Include (optional) +- Files to Exclude? (optional) + +` + + + +export const ORIGINAL = `<<<<<<< ORIGINAL` +export const DIVIDER = `=======` +export const FINAL = `>>>>>>> UPDATED` + +export const searchReplace_systemMessage = `\ +You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file. + +A SEARCH/REPLACE block describes the code before and after a change. Here is the format: +${ORIGINAL} +// ... original code goes here +${DIVIDER} +// ... final code goes here +${FINAL} + +You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make. +Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks. + +Directions: +1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. +2. The "original" code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. +3. The "original" code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. +4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. +- Make sure you add all necessary imports. +- Make sure the "final" code is complete and will not result in syntax/lint errors. +5. Follow coding convention (spaces, semilcolons, comments, etc). + +## EXAMPLE 1 +ORIGINAL_FILE +${tripleTick[0]} +let w = 5 +let x = 6 +let y = 7 +let z = 8 +${tripleTick[1]} + +CHANGE +Make x equal to 6.5, not 6. +${tripleTick[0]} +// ... existing code +let x = 6.5 +// ... existing code +${tripleTick[1]} + + +## ACCEPTED OUTPUT +${tripleTick[0]} +${ORIGINAL} +let x = 6 +${DIVIDER} +let x = 6.5 +${FINAL} +${tripleTick[1]} +` + +export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ +ORIGINAL_FILE +${originalCode} + +CHANGE +${applyStr} + +INSTRUCTIONS +Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation. +` diff --git a/src/vs/workbench/contrib/void/browser/searchAndReplaceService.ts b/src/vs/workbench/contrib/void/browser/searchAndReplaceService.ts deleted file mode 100644 index c668ae26..00000000 --- a/src/vs/workbench/contrib/void/browser/searchAndReplaceService.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; - - - -export type ChatMessageLocation = { - threadId: string; - messageIdx: number; -} - - -export type SearchAndReplaceBlock = { - search: string; - replace: string; -} - -// service that manages state -export type ApplyState = { - [applyBoxId: string]: { - searchAndReplaceBlocks: SearchAndReplaceBlock; - } -} - -// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion` - -export interface IFastApplyService { - readonly _serviceBrand: undefined; - - // readonly state: ApplyState; // readonly to the user - // setState(newState: Partial): void; - // onDidChangeState: Event; -} - -export const IVoidFastApplyService = createDecorator('voidFastApplyService'); -class VoidFastApplyService extends Disposable implements IFastApplyService { - _serviceBrand: undefined; - - static readonly ID = 'voidFastApplyService'; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - - - // state - // state: ApplyState - - constructor( - ) { - super() - - // initial state - // this.state = { currentUri: undefined } - } - - setState(newState: Partial) { - - // this.state = { ...this.state, ...newState } - this._onDidChangeState.fire() - } - - -} - -registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 8ffd6b9b..1eda93d1 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -161,7 +161,7 @@ export class ToolService implements IToolService { const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)); const data = await searchService.textSearch(query, CancellationToken.None); - const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n') + const str = data.results.map(({ resource, results }) => resource) return str }, @@ -171,6 +171,8 @@ export class ToolService implements IToolService { } + + callContextTool: IToolService['callContextTool'] = (toolName, params) => { return this.contextToolCallFns[toolName](params) } From 343ee5eb9483993b163cb1310717dcca2e465ebd Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 13 Feb 2025 22:59:59 -0800 Subject: [PATCH 11/92] Apply name --- .../workbench/contrib/void/browser/inlineDiffsService.ts | 4 ++-- .../workbench/contrib/void/common/voidSettingsService.ts | 4 ++-- src/vs/workbench/contrib/void/common/voidSettingsTypes.ts | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index ed436b61..f8449720 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -1238,7 +1238,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // TODO turn this into a service and provide it streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', - useProviderFor: 'FastApply', + useProviderFor: 'Apply', logging: { loggingName: `generateSearchAndReplace` }, messages, onText: ({ fullText }) => { @@ -1478,7 +1478,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', - useProviderFor: opts.from === 'ClickApply' ? 'FastApply' : 'Ctrl+K', + useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K', logging: { loggingName: `startApplying - ${from}` }, messages, onText: ({ newText: newText_ }) => { diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 4322eaf4..59089230 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -131,7 +131,7 @@ const _updatedValidatedState = (state: Omit) const defaultState = () => { const d: VoidSettingsState = { settingsOfProvider: deepClone(defaultSettingsOfProvider), - modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'FastApply': null }, + modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null }, globalSettings: deepClone(defaultGlobalSettings), _modelOptions: [], // computed later } @@ -189,7 +189,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const newModelSelectionOfFeature = { // A HACK BECAUSE WE ADDED FastApply - ...{ 'FastApply': null }, + ...{ 'Apply': null }, ...readS.modelSelectionOfFeature, } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 0a5bdc64..a31eb771 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -436,18 +436,20 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => } // this is a state -export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'FastApply'] as const +export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'Apply'] as const export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null> export type FeatureName = keyof ModelSelectionOfFeature export const displayInfoOfFeatureName = (featureName: FeatureName) => { + // editor: if (featureName === 'Autocomplete') return 'Autocomplete' else if (featureName === 'Ctrl+K') - return 'Quick-Edit' + return 'Quick Edit' + // sidebar: else if (featureName === 'Ctrl+L') return 'Chat' - else if (featureName === 'FastApply') + else if (featureName === 'Apply') return 'Apply' else throw new Error(`Feature Name ${featureName} not allowed`) From 0bcd88dad6fd7e28d52b69a291d7b19132ebcff3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 13 Feb 2025 23:54:22 -0800 Subject: [PATCH 12/92] fix --- .../contrib/void/browser/aiRegexService.ts | 130 +++++++++--------- .../browser/helpers/extractCodeFromResult.ts | 2 +- .../contrib/void/browser/prompt/prompts.ts | 6 +- .../contrib/void/common/toolsService.ts | 2 +- .../electron-main/llmMessage/anthropic.ts | 32 ++++- .../void/electron-main/llmMessage/openai.ts | 13 +- 6 files changed, 110 insertions(+), 75 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/aiRegexService.ts b/src/vs/workbench/contrib/void/browser/aiRegexService.ts index 9f96da76..c0ae27fa 100644 --- a/src/vs/workbench/contrib/void/browser/aiRegexService.ts +++ b/src/vs/workbench/contrib/void/browser/aiRegexService.ts @@ -5,10 +5,10 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { URI } from '../../../../base/common/uri.js'; +// import { URI } from '../../../../base/common/uri.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IToolService, ToolService } from '../common/toolsService.js'; +// import { IToolService, ToolService } from '../common/toolsService.js'; @@ -54,7 +54,7 @@ class VoidFastApplyService extends Disposable implements IFastApplyService { // state: ApplyState constructor( - @IToolService private readonly toolService: ToolService + // @IToolService private readonly toolService: ToolService ) { super() @@ -88,97 +88,97 @@ class VoidFastApplyService extends Disposable implements IFastApplyService { // -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error) - private async _searchUsingAI({ searchClause }: { searchClause: string }) { + // private async _searchUsingAI({ searchClause }: { searchClause: string }) { - const relevantURIs: URI[] = [] - const gatherPrompt = `\ -asdasdas -` - const filterPrompt = `\ -Is this file relevant? -` + // // const relevantURIs: URI[] = [] + // // const gatherPrompt = `\ + // // asdasdas + // // ` + // // const filterPrompt = `\ + // // Is this file relevant? + // // ` - // optimizations (DO THESE LATER!!!!!!) - // if tool includes a uri in uriSet, skip it obviously - let uriSet = new Set() - // gather - let messages = [] - while (true) { - const result = await new Promise((res, rej) => { - sendLLMMessage({ - messages, - tools: ['search'], - onFinalMessage: ({ result: r, }) => { - res(r) - }, - onError: (error) => { - rej(error) - } - }) - }) + // // // optimizations (DO THESE LATER!!!!!!) + // // // if tool includes a uri in uriSet, skip it obviously + // // let uriSet = new Set() + // // // gather + // // let messages = [] + // // while (true) { + // // const result = await new Promise((res, rej) => { + // // sendLLMMessage({ + // // messages, + // // tools: ['search'], + // // onFinalMessage: ({ result: r, }) => { + // // res(r) + // // }, + // // onError: (error) => { + // // rej(error) + // // } + // // }) + // // }) - messages.push({ role: 'tool', content: turnToString(result) }) + // // messages.push({ role: 'tool', content: turnToString(result) }) - sendLLMMessage({ - messages: { 'Output ': result }, - onFinalMessage: (r) => { - // output is file1\nfile2\nfile3\n... - } - }) + // // sendLLMMessage({ + // // messages: { 'Output ': result }, + // // onFinalMessage: (r) => { + // // // output is file1\nfile2\nfile3\n... + // // } + // // }) - uriSet.add(...) - } + // // uriSet.add(...) + // // } - // writes - if (!replaceClause) return + // // // writes + // // if (!replaceClause) return - for (const uri of uriSet) { - // in future, batch these - applyWorkflow({ uri, applyStr: replaceClause }) - } + // // for (const uri of uriSet) { + // // // in future, batch these + // // applyWorkflow({ uri, applyStr: replaceClause }) + // // } - // while (true) { - // const result = new Promise((res, rej) => { - // sendLLMMessage({ - // messages, - // tools: ['search'], - // onResult: (r) => { - // res(r) - // } - // }) - // }) + // // while (true) { + // // const result = new Promise((res, rej) => { + // // sendLLMMessage({ + // // messages, + // // tools: ['search'], + // // onResult: (r) => { + // // res(r) + // // } + // // }) + // // }) - // messages.push(result) + // // messages.push(result) - // } + // // } - } + // } - private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) { + // private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) { - for (const uri of relevantURIs) { + // for (const uri of relevantURIs) { - uri + // uri - } + // } - // should I change this file? - // if so what changes to make? + // // should I change this file? + // // if so what changes to make? - // fast apply the changes - } + // // fast apply the changes + // } diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index bb1ed350..b7665eca 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts' +import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js' class SurroundingsRemover { readonly originalS: string diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 9185e0dc..45a573ae 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -224,7 +224,7 @@ Please finish writing the new file by applying the change to the original file. -const aiRegex_computeReplacementsForFile_systemMessage = `\ +export const aiRegex_computeReplacementsForFile_systemMessage = `\ You are a "search and replace" coding assistant. You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE. @@ -246,7 +246,7 @@ For example, if the user is asking you to "make this variable a better name", ma - Make sure you give enough context in the code block to apply the changes to the correct location in the code` -const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService }) => { +export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService }) => { // we may want to do this in batches const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null } @@ -273,7 +273,7 @@ Please return the changes you want to make to the file in a codeblock, or return // don't have to tell it it will be given the history; just give it to it -const aiRegex_search_systemMessage = `\ +export const aiRegex_search_systemMessage = `\ You are a coding assistant that executes the SEARCH part of a user's search and replace query. You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context. diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 1eda93d1..c8b8bb2d 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -162,7 +162,7 @@ export class ToolService implements IToolService { const data = await searchService.textSearch(query, CancellationToken.None); const str = data.results.map(({ resource, results }) => resource) - return str + return str as any }, } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index 91461b16..97cd3ed9 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -53,11 +53,37 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText({ newText, fullText }) }) + + // can do tool use streaming + const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} + stream.on('streamEvent', e => { + if (e.type === 'content_block_start') { + if (e.content_block.type !== 'tool_use') return + const index = e.index + const tool = e.content_block + if (!toolCallOfIndex[index]) + toolCallOfIndex[index] = { name: '', args: '' } + toolCallOfIndex[index].name += tool.name ?? '' + toolCallOfIndex[index].args += tool.input ?? '' + + } + else if (e.type === 'content_block_delta') { + if (e.delta.type !== 'input_json_delta') return + toolCallOfIndex[e.index].args += e.delta.partial_json + } + // TODO!!!!! + // onText({}) + }) + // when we get the final message on this stream (or when error/fail) - stream.on('finalMessage', (claude_response) => { + stream.on('finalMessage', (response) => { // stringify the response's content - const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n'); - onFinalMessage({ fullText: content }) + const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n') + const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, input: c.input } : null) + + console.log("TOOLS!!!!", typeof tools[0]?.input, JSON.stringify(tools, null, 2)) + + onFinalMessage({ fullText: content, }) }) stream.on('error', (error) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index df4d2322..30e80bc6 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -121,6 +121,7 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => { let fullText = '' + const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { @@ -137,11 +138,19 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on // when receive text for await (const chunk of response) { + // tool call + for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) { + const index = tool.index + if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' } + toolCallOfIndex[index].name += tool.function?.name ?? '' + toolCallOfIndex[index].args += tool.function?.arguments ?? '' + } + + // message let newText = '' - newText += chunk.choices[0]?.delta?.tool_calls?.[0]?.function?.name ?? '' - newText += chunk.choices[0]?.delta?.tool_calls?.[0]?.function?.arguments ?? '' newText += chunk.choices[0]?.delta?.content ?? '' fullText += newText; + onText({ newText, fullText }); } onFinalMessage({ fullText }); From 9cfcf396c1bd51a9b68e30d29b2d88a78243853f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 14 Feb 2025 01:05:10 -0800 Subject: [PATCH 13/92] update types for tool support --- .../contrib/void/common/llmMessageTypes.ts | 5 ++ .../contrib/void/common/toolsService.ts | 56 ++++++++----------- .../electron-main/llmMessage/anthropic.ts | 18 +++--- .../void/electron-main/llmMessage/ollama.ts | 5 ++ .../void/electron-main/llmMessage/openai.ts | 6 +- .../llmMessage/sendLLMMessage.ts | 15 ++--- 6 files changed, 53 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index fb6a94fc..40ab1f2a 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { InternalToolInfo } from './toolsService.js' import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' @@ -44,9 +45,11 @@ type _InternalSendFIMMessage = { type SendLLMType = { messagesType: 'chatMessages'; messages: LLMChatMessage[]; + tools?: InternalToolInfo[]; } | { messagesType: 'FIMMessage'; messages: _InternalSendFIMMessage; + tools?: undefined; } // service types @@ -96,6 +99,8 @@ export type _InternalSendLLMChatMessageFnType = ( modelName: string; _setAborter: (aborter: () => void) => void; + tools?: InternalToolInfo[], + messages: _InternalLLMChatMessage[]; } ) => void diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index c8b8bb2d..86cc4356 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -15,6 +15,7 @@ import { ISearchService } from '../../../../workbench/services/search/common/sea // we do this using Anthropic's style and convert to OpenAI style later export type InternalToolInfo = { + name: string, description: string, params: { [paramName: string]: { type: string, description: string | undefined } // name -> type @@ -23,13 +24,14 @@ export type InternalToolInfo = { } // helper -const pagination = { +const paginationHelper = { desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } } as const export const contextTools = { read_file: { + name: 'read_file', description: 'Returns file contents of a given URI.', params: { uri: { type: 'string', description: undefined }, @@ -38,28 +40,31 @@ export const contextTools = { }, list_dir: { - description: `Returns all file names and folder names in a given URI. ${pagination.desc}`, + name: 'list_dir', + description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, params: { uri: { type: 'string', description: undefined }, - ...pagination.param + ...paginationHelper.param }, required: ['uri'], }, pathname_search: { - description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${pagination.desc}`, + name: 'pathname_search', + description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, params: { query: { type: 'string', description: undefined }, - ...pagination.param, + ...paginationHelper.param, }, required: ['query'] }, search: { - description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${pagination.desc}`, + name: 'search', + description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, params: { query: { type: 'string', description: undefined }, - ...pagination.param, + ...paginationHelper.param, }, required: ['query'], }, @@ -69,26 +74,18 @@ export const contextTools = { // // RAG // }, -} as const satisfies { [name: string]: InternalToolInfo } +} export type ContextToolName = keyof typeof contextTools type ContextToolParamNames = keyof typeof contextTools[T]['params'] type ContextToolParams = { [paramName in ContextToolParamNames]: unknown } -type AllContextToolCallFns = { - [ToolName in ContextToolName]: ((p: (ContextToolParams)) => Promise) -} - -// TODO check to make sure in workspace -// TODO check to make sure is not gitignored - - async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise { let output = '' function traverseChildren(children: IFileStat[], depth: number) { @@ -116,7 +113,6 @@ const validateURI = (uriStr: unknown) => { export interface IToolService { readonly _serviceBrand: undefined; - callContextTool: (toolName: T, params: ContextToolParams) => Promise } export const IToolService = createDecorator('ToolService'); @@ -125,7 +121,7 @@ export class ToolService implements IToolService { readonly _serviceBrand: undefined; - contextToolCallFns: AllContextToolCallFns + public contextToolCallFns constructor( @IFileService fileService: IFileService, @@ -138,31 +134,33 @@ export class ToolService implements IToolService { const queryBuilder = instantiationService.createInstance(QueryBuilder); this.contextToolCallFns = { - read_file: async ({ uri: uriStr }) => { + read_file: async ({ uri: uriStr }: ContextToolParams<'read_file'>) => { const uri = validateURI(uriStr) const fileContents = await VSReadFileRaw(fileService, uri) return fileContents ?? '(could not read file)' }, - list_dir: async ({ uri: uriStr }) => { + list_dir: async ({ uri: uriStr }: ContextToolParams<'list_dir'>) => { const uri = validateURI(uriStr) + // TODO!!!! check to make sure in workspace + // TODO check to make sure is not gitignored const treeStr = await generateDirectoryTreeMd(fileService, uri) return treeStr }, - pathname_search: async ({ query: queryStr }) => { + pathname_search: async ({ query: queryStr }: ContextToolParams<'pathname_search'>) => { if (typeof queryStr !== 'string') return '(Error: query was not a string)' const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }); const data = await searchService.fileSearch(query, CancellationToken.None); - const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n') - return str + const URIs = data.results.map(({ resource, results }) => resource.fsPath) + return URIs }, - search: async ({ query: queryStr }) => { + search: async ({ query: queryStr }: ContextToolParams<'search'>) => { if (typeof queryStr !== 'string') return '(Error: query was not a string)' const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)); const data = await searchService.textSearch(query, CancellationToken.None); - const str = data.results.map(({ resource, results }) => resource) - return str as any + const URIs = data.results.map(({ resource, results }) => resource) + return URIs }, } @@ -172,12 +170,6 @@ export class ToolService implements IToolService { } - - callContextTool: IToolService['callContextTool'] = (toolName, params) => { - return this.contextToolCallFns[toolName](params) - } - - } registerSingleton(IToolService, ToolService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index 97cd3ed9..e957c83a 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -11,10 +11,10 @@ import { InternalToolInfo } from '../../common/toolsService.js'; -export const toAnthropicTool = (toolName: string, toolInfo: InternalToolInfo) => { - const { description, params, required } = toolInfo +export const toAnthropicTool = (toolInfo: InternalToolInfo) => { + const { name, description, params, required } = toolInfo return { - name: toolName, + name: name, description: description, input_schema: { type: 'object', @@ -45,6 +45,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, messages: messages, model: modelName, max_tokens: maxTokens, + // tools: [toAnthropicTool(contextTools.list_dir)] }); @@ -60,12 +61,9 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, if (e.type === 'content_block_start') { if (e.content_block.type !== 'tool_use') return const index = e.index - const tool = e.content_block - if (!toolCallOfIndex[index]) - toolCallOfIndex[index] = { name: '', args: '' } - toolCallOfIndex[index].name += tool.name ?? '' - toolCallOfIndex[index].args += tool.input ?? '' - + if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' } + toolCallOfIndex[index].name += e.content_block.name ?? '' + toolCallOfIndex[index].args += e.content_block.input ?? '' } else if (e.type === 'content_block_delta') { if (e.delta.type !== 'input_json_delta') return @@ -79,7 +77,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, stream.on('finalMessage', (response) => { // stringify the response's content const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n') - const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, input: c.input } : null) + const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, input: c.input } : null).filter(c => !!c) console.log("TOOLS!!!!", typeof tools[0]?.input, JSON.stringify(tools, null, 2)) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts index 43c817a3..5a69698b 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts @@ -99,6 +99,11 @@ export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, on // iterate through the stream for await (const chunk of stream) { const newText = chunk.message.content; + + + + // chunk.message.tool_calls[0].function.arguments + fullText += newText; onText({ newText, fullText }); } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 30e80bc6..ead592d3 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -14,12 +14,12 @@ import { InternalToolInfo } from '../../common/toolsService.js'; // prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting -export const toOpenAITool = (toolName: string, toolInfo: InternalToolInfo) => { - const { description, params, required } = toolInfo +export const toOpenAITool = (toolInfo: InternalToolInfo) => { + const { name, description, params, required } = toolInfo return { type: 'function', function: { - name: toolName, + name: name, description: description, parameters: { type: 'object', diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index dc70c36c..86a77362 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -61,6 +61,7 @@ export const sendLLMMessage = ({ settingsOfProvider, providerName, modelName, + tools, }: SendLLMMessageParams, metricsService: IMetricsService @@ -141,27 +142,27 @@ export const sendLLMMessage = ({ case 'deepseek': case 'openAICompatible': if (messagesType === 'FIMMessage') sendOpenAIFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); - else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'ollama': - if (messagesType === 'FIMMessage') sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) - else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) + if (messagesType === 'FIMMessage') sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'anthropic': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' }) - else /* */ sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + else /* */ sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'gemini': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM' }) - else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'groq': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM' }) - else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'mistral': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM' }) - else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) From 0975f1bf5f6cc01a7d161809ec196200f84d3fce Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 14 Feb 2025 01:52:17 -0800 Subject: [PATCH 14/92] add developer info for models --- .../void/common/voidSettingsService.ts | 23 +- .../contrib/void/common/voidSettingsTypes.ts | 292 ++++++++++++------ .../void/electron-main/templates/templates.ts | 13 - 3 files changed, 206 insertions(+), 122 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/electron-main/templates/templates.ts diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 59089230..e44a294a 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IMetricsService } from './metricsService.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultModelNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings, developerInfoOfRecognizedModel, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; const STORAGE_KEY = 'void.settingsServiceStorage' @@ -289,27 +289,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { - setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) { + setAutodetectedModels(providerName: ProviderName, autodetectedModelNames: string[], logging: object) { const { models } = this.state.settingsOfProvider[providerName] - const oldModelNames = models.map(m => m.modelName) - const newDefaultModelInfo = modelInfoOfDefaultModelNames(newDefaultModelNames, { isAutodetected: true, existingModels: models }) - const newModelInfo = [ - ...newDefaultModelInfo, // swap out all the default models for the new default models - ...models.filter(m => !m.isDefault), // keep any non-defaul (custom) models + + const newDefaultModels = modelInfoOfAutodetectedModelNames(autodetectedModelNames, { existingModels: models }) + const newModels = [ + ...newDefaultModels, // swap out all the default models for the new default models + ...models.filter(m => !m.isDefault), // keep any non-default (custom) models ] - - this.setSettingOfProvider(providerName, 'models', newModelInfo) + this.setSettingOfProvider(providerName, 'models', newModels) // if the models changed, log it - const new_names = newModelInfo.map(m => m.modelName) + const new_names = newModels.map(m => m.modelName) if (!(oldModelNames.length === new_names.length && oldModelNames.every((_, i) => oldModelNames[i] === new_names[i])) ) { - this._metricsService.capture('Autodetect Models', { providerName, newModels: newModelInfo, ...logging }) + this._metricsService.capture('Autodetect Models', { providerName, newModels: newModels, ...logging }) } } toggleModelHidden(providerName: ProviderName, modelName: string) { @@ -335,7 +334,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { if (existingIdx !== -1) return // if exists, do nothing const newModels = [ ...models, - { modelName, isDefault: false, isHidden: false } + { ...developerInfoOfRecognizedModel(modelName), modelName, isDefault: false, isHidden: false } ] this.setSettingOfProvider(providerName, 'models', newModels) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index a31eb771..10bee1c0 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -7,45 +7,217 @@ import { VoidSettingsState } from './voidSettingsService.js' -export type VoidModelInfo = { + +// developer info used in sendLLMMessage +type VoidModelDeveloperInfo = { + supportsSystemMessage: 'system' | 'developer' | false, // if null, we will just do a string of system message + supportsTools: boolean, // we will just do a string of tool use if it doesn't support + supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> + supportsStreaming: boolean, // (o1 does NOT) we will just dump the final result if doesn't support it + maxTokens: number, // required, DEFAULT is Infinity +} + +export type VoidModelInfo = { // <-- STATEFUL modelName: string, isDefault: boolean, // whether or not it's a default for its provider isHidden: boolean, // whether or not the user is hiding it (switched off) isAutodetected?: boolean, // whether the model was autodetected by polling -} +} & VoidModelDeveloperInfo -// creates `modelInfo` from `modelNames` -export const modelInfoOfDefaultModelNames = (defaultModelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => { - const { isAutodetected, existingModels } = options ?? {} - if (!existingModels) { // default settings - return defaultModelNames.map((modelName, i) => ({ - modelName, - isDefault: true, - isAutodetected: isAutodetected, - isHidden: defaultModelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually - })) - } else { // settings if there are existing models (keep existing `isHidden` property) - const existingModelsMap: Record = {} - for (const existingModel of existingModels) { - existingModelsMap[existingModel.modelName] = existingModel - } - return defaultModelNames.map((modelName, i) => ({ - modelName, - isDefault: true, - isAutodetected: isAutodetected, - isHidden: !!existingModelsMap[modelName]?.isHidden, - })) +export const recognizedModels = [ + // chat + 'OpenAI 4o', + 'Anthropic Claude', + 'Llama 3.x', + 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model + // 'xAI Grok', + // 'Google Gemini, Gemma', + // 'Microsoft Phi4', + + + // coding (autocomplete) + 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 + 'Mistral Codestral', + + // thinking + 'OpenAI o1, o3', + 'Deepseek R1', + + // general + // 'Mixtral 8x7b' + // 'Qwen2.5', + +] as const + + + + +type RecognizedModel = (typeof recognizedModels)[number] | '' + + +// const modelCapabilities: { [recognizedModel in RecognizedModel]: ({ }) => string } = { +// 'OpenAI 4o': { +// template: ({ prefix, suffix, }: { prefix: string; suffix: string; }) => `\ +// ` +// } +// } + +export function getRecognizedModel(modelName: string): RecognizedModel { + const lower = modelName.toLowerCase(); + + if (lower.includes('gpt-4o')) { + return 'OpenAI 4o'; + } + if (lower.includes('claude')) { + return 'Anthropic Claude'; + } + if (lower.includes('llama')) { + return 'Llama 3.x'; + } + if (lower.includes('qwen2.5-coder')) { + return 'Alibaba Qwen2.5 Coder Instruct'; + } + if (lower.includes('mistral')) { + return 'Mistral Codestral'; + } + // Check for "o1" or "o3" + if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) { + return 'OpenAI o1, o3'; + } + if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) { + return 'Deepseek R1'; } + // Fallback: + return ''; } + + +export const developerInfoOfRecognizedModel = (modelName: string) => { + const devInfo: { [recognizedModel in RecognizedModel]: VoidModelDeveloperInfo } = { + 'OpenAI 4o': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + 'Anthropic Claude': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + 'Llama 3.x': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + 'Deepseek Chat': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + 'Alibaba Qwen2.5 Coder Instruct': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + 'Mistral Codestral': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + 'OpenAI o1, o3': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + 'Deepseek R1': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + '': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + } + + + const modelName_ = getRecognizedModel(modelName) + return devInfo[modelName_] +} + + + + + + +// creates `modelInfo` from `modelNames` +export const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): VoidModelInfo[] => { + return defaultModelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: false, + isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually + ...developerInfoOfRecognizedModel(modelName) + })) +} + +export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { + const { existingModels } = options + + const existingModelsMap: Record = {} + for (const existingModel of existingModels) { + existingModelsMap[existingModel.modelName] = existingModel + } + + return defaultModelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: true, + isHidden: !!existingModelsMap[modelName]?.isHidden, + ...developerInfoOfRecognizedModel(modelName) + })) +} + + + + + // https://docs.anthropic.com/en/docs/about-claude/models export const defaultAnthropicModels = modelInfoOfDefaultModelNames([ 'claude-3-5-sonnet-20241022', @@ -530,77 +702,3 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe - - - -export const recognizedModels = [ - - // chat - 'OpenAI 4o', - 'Anthropic Claude', - 'Llama 3.x', - 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model - // 'xAI Grok', - // 'Google Gemini, Gemma', - // 'Microsoft Phi4', - - - // coding (autocomplete) - 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 - 'Mistral Codestral', - - // thinking - 'OpenAI o1, o3', - 'Deepseek R1', - - // general - '' - // 'Mixtral 8x7b' - // 'Qwen2.5', - -] as const - - - - -type RecognizedModel = (typeof recognizedModels)[number] - - -// const modelCapabilities: { [recognizedModel in RecognizedModel]: ({ }) => string } = { -// 'OpenAI 4o': { -// template: ({ prefix, suffix, }: { prefix: string; suffix: string; }) => `\ -// ` -// } -// } - -export function getRecognizedModel(modelName: string): RecognizedModel { - const lower = modelName.toLowerCase(); - - if (lower.includes('gpt-4o')) { - return 'OpenAI 4o'; - } - if (lower.includes('claude')) { - return 'Anthropic Claude'; - } - if (lower.includes('llama')) { - return 'Llama 3.x'; - } - if (lower.includes('qwen2.5-coder')) { - return 'Alibaba Qwen2.5 Coder Instruct'; - } - if (lower.includes('mistral')) { - return 'Mistral Codestral'; - } - // Check for "o1" or "o3" - if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) { - return 'OpenAI o1, o3'; - } - if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) { - return 'Deepseek R1'; - } - - - - // Fallback: - return ''; -} diff --git a/src/vs/workbench/contrib/void/electron-main/templates/templates.ts b/src/vs/workbench/contrib/void/electron-main/templates/templates.ts deleted file mode 100644 index 138f7be3..00000000 --- a/src/vs/workbench/contrib/void/electron-main/templates/templates.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - -modelName -> { - system_message_type: 'system' | 'developer' (openai) | null // if null, we will just do a string of system message - supports_tools: boolean // we will just do a string of tool use if it doesn't support - supports_autocomplete_FIM (suffix) // we will just do a description of FIM if it doens't support <|fim_hole|> - - supports_streaming: boolean // (o1 does NOT) we will just dump the final result if doesn't support it - max_tokens: number // required, DEFAULT is Infinity - -} - -*/ From 6ad48ffa20c71cd437de15cbd5151b0fe10c4551 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 14 Feb 2025 01:59:17 -0800 Subject: [PATCH 15/92] recognizedModels --- .../contrib/void/common/voidSettingsTypes.ts | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 10bee1c0..5d371b5d 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -9,14 +9,23 @@ import { VoidSettingsState } from './voidSettingsService.js' // developer info used in sendLLMMessage -type VoidModelDeveloperInfo = { - supportsSystemMessage: 'system' | 'developer' | false, // if null, we will just do a string of system message +export type VoidModelDeveloperInfo = { + // USED: + + // TODO!!!! + // UNUSED (coming soon): + recognizedModelName: RecognizedModel, // used to show user if model was auto-recognized supportsTools: boolean, // we will just do a string of tool use if it doesn't support + supportsSystemMessage: 'system' | 'developer' | false, // if null, we will just do a string of system message supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> supportsStreaming: boolean, // (o1 does NOT) we will just dump the final result if doesn't support it maxTokens: number, // required, DEFAULT is Infinity } + + + + export type VoidModelInfo = { // <-- STATEFUL modelName: string, isDefault: boolean, // whether or not it's a default for its provider @@ -28,9 +37,6 @@ export type VoidModelInfo = { // <-- STATEFUL - - - export const recognizedModels = [ // chat 'OpenAI 4o', @@ -102,7 +108,7 @@ export function getRecognizedModel(modelName: string): RecognizedModel { export const developerInfoOfRecognizedModel = (modelName: string) => { - const devInfo: { [recognizedModel in RecognizedModel]: VoidModelDeveloperInfo } = { + const devInfo: { [recognizedModel in RecognizedModel]: Omit } = { 'OpenAI 4o': { supportsSystemMessage: false, supportsTools: false, @@ -176,9 +182,12 @@ export const developerInfoOfRecognizedModel = (modelName: string) => { }, } + const recognizedModelName = getRecognizedModel(modelName) - const modelName_ = getRecognizedModel(modelName) - return devInfo[modelName_] + return { + recognizedModelName: recognizedModelName, + ...devInfo[recognizedModelName], + } } @@ -193,7 +202,7 @@ export const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): VoidM isDefault: true, isAutodetected: false, isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually - ...developerInfoOfRecognizedModel(modelName) + ...developerInfoOfRecognizedModel(modelName), })) } From 1839acab1f40fbf44c6e9283850461da5ad3e121 Mon Sep 17 00:00:00 2001 From: adrieljss Date: Fri, 14 Feb 2025 17:59:56 +0800 Subject: [PATCH 16/92] fix ENOENT error spawn on Windows --- package-lock.json | 10 +++------- package.json | 1 + src/vs/workbench/contrib/void/browser/react/build.js | 3 ++- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc045891..f1265773 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@xterm/headless": "^5.6.0-beta.64", "@xterm/xterm": "^5.6.0-beta.64", "ajv": "^8.17.1", + "cross-spawn": "^7.0.6", "diff": "^7.0.0", "groq-sdk": "^0.9.0", "http-proxy-agent": "^7.0.0", @@ -7116,7 +7117,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -14249,8 +14250,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", @@ -17536,7 +17536,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -20241,7 +20240,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -20253,7 +20251,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -23733,7 +23730,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index a4ee38bb..31b355b4 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "@xterm/headless": "^5.6.0-beta.64", "@xterm/xterm": "^5.6.0-beta.64", "ajv": "^8.17.1", + "cross-spawn": "^7.0.6", "diff": "^7.0.0", "groq-sdk": "^0.9.0", "http-proxy-agent": "^7.0.0", diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js index ece3ebf6..ab4ea921 100755 --- a/src/vs/workbench/contrib/void/browser/react/build.js +++ b/src/vs/workbench/contrib/void/browser/react/build.js @@ -3,7 +3,8 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { spawn, execSync } from 'child_process'; +import { execSync } from 'child_process'; +import { spawn } from 'cross-spawn' // Added lines below import fs from 'fs'; import path from 'path'; From 198a948f6c060c52399cb4d832a6de9d59aedad0 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 14 Feb 2025 21:30:20 -0800 Subject: [PATCH 17/92] star button commit --- .../browser/parts/editor/editorActions.ts | 20 +++++++++++++++- .../editor/media/multieditortabscontrol.css | 18 ++++----------- .../parts/editor/multiEditorTabsControl.ts | 23 ++++++++++--------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 1b1a39b3..39c4275b 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -432,7 +432,7 @@ export class UnpinEditorAction extends Action { label: string, @ICommandService private readonly commandService: ICommandService ) { - super(id, label, ThemeIcon.asClassName(Codicon.pinned)); + super(id, label, ThemeIcon.asClassName(Codicon.starFull)); } override run(context?: IEditorCommandsContext): Promise { @@ -440,6 +440,24 @@ export class UnpinEditorAction extends Action { } } +export class PinEditorAction extends Action { + + static readonly ID = 'workbench.action.pinEditor'; + static readonly LABEL = localize('pinEditor', "Pin Editor"); + + constructor( + id: string, + label: string, + @ICommandService private readonly commandService: ICommandService + ) { + super(id, label, ThemeIcon.asClassName(Codicon.star)); + } + + override async run(context?: IEditorCommandsContext): Promise { + return this.commandService.executeCommand('workbench.action.pinEditor', undefined, context); + } +} + export class CloseEditorTabAction extends Action { static readonly ID = 'workbench.action.closeActiveEditor'; diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 93559402..dd6c233f 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -385,7 +385,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-fixed > .tab-actions { flex: 0; - overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink/fixed to make more room */ + overflow: visible; /* ensure tab actions are always visible */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions, @@ -399,18 +399,8 @@ overflow: visible; /* ...but still show the tab actions on hover, focus and when dirty or sticky */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-action-off:not(.dirty) > .tab-actions, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky-compact > .tab-actions { - display: none; /* hide the tab actions when we are configured to hide it (unless dirty, but always when sticky-compact) */ -} - -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-actions .action-label, /* always show tab actions for active tab */ -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-actions .action-label:focus, /* always show tab actions on focus */ -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-actions .action-label, /* always show tab actions on hover */ -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-actions .action-label, /* always show tab actions on hover */ -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, /* always show tab actions for sticky tabs */ -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-actions .action-label { /* always show tab actions for dirty tabs */ - opacity: 1; + display: none; /* only hide tab actions when sticky-compact */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .actions-container { @@ -444,11 +434,11 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-actions .action-label { - opacity: 0.5; /* show tab actions dimmed for inactive group */ + opacity: 1; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .action-label { - opacity: 0; + opacity: 1; } /* Tab Actions: Off */ diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index b23be82e..bbda32c4 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -35,7 +35,7 @@ import { MergeGroupMode, IMergeGroupOptions } from '../../../services/editor/com import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from './editor.js'; -import { CloseEditorTabAction, UnpinEditorAction } from './editorActions.js'; +import { CloseEditorTabAction, PinEditorAction, UnpinEditorAction } from './editorActions.js'; import { assertAllDefined, assertIsDefined } from '../../../../base/common/types.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { basenameOrAuthority } from '../../../../base/common/resources.js'; @@ -113,6 +113,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseEditorTabAction, CloseEditorTabAction.ID, CloseEditorTabAction.LABEL)); private readonly unpinEditorAction = this._register(this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL)); + private readonly pinEditorAction = this._register(this.instantiationService.createInstance(PinEditorAction, PinEditorAction.ID, PinEditorAction.LABEL)); // Add this line private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); private tabLabels: IEditorInputLabel[] = []; @@ -1518,28 +1519,28 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel); // Action - const hasUnpinAction = isTabSticky && options.tabActionUnpinVisibility; - const hasCloseAction = !hasUnpinAction && options.tabActionCloseVisibility; - const hasAction = hasUnpinAction || hasCloseAction; + const hasCloseAction = options.tabActionCloseVisibility; + const hasAction = true; // Always show actions + // Determine which action to show let tabAction; - if (hasAction) { - tabAction = hasUnpinAction ? this.unpinEditorAction : this.closeEditorAction; + if (isTabSticky) { + tabAction = this.unpinEditorAction; } else { - // Even if the action is not visible, add it as it contains the dirty indicator - tabAction = isTabSticky ? this.unpinEditorAction : this.closeEditorAction; + tabAction = this.pinEditorAction; // Use pin action instead of close action } + // Update action bar if (!tabActionBar.hasAction(tabAction)) { if (!tabActionBar.isEmpty()) { tabActionBar.clear(); } - tabActionBar.push(tabAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(tabAction) }); } - tabContainer.classList.toggle(`pinned-action-off`, isTabSticky && !hasUnpinAction); - tabContainer.classList.toggle(`close-action-off`, !hasUnpinAction && !hasCloseAction); + tabContainer.classList.toggle('sticky', isTabSticky); + tabContainer.classList.toggle(`pinned-action-off`, false); + tabContainer.classList.toggle(`close-action-off`, !hasCloseAction); for (const option of ['left', 'right']) { tabContainer.classList.toggle(`tab-actions-${option}`, hasAction && options.tabActionLocation === option); From d91ec9da2e07fefb7f10932b67491c9a7147b54a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 14 Feb 2025 21:43:38 -0800 Subject: [PATCH 18/92] inlineDiffsService -> editCodeService --- ...lineDiffsService.ts => editCodeService.ts} | 26 +++++-------------- .../contrib/void/browser/quickEditActions.ts | 6 ++--- .../react/src/markdown/ChatMarkdownRender.tsx | 8 +++--- .../src/quick-edit-tsx/QuickEditChat.tsx | 14 +++++----- .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../void/browser/react/src/util/services.tsx | 8 +++--- .../contrib/void/browser/void.contribution.ts | 2 +- 7 files changed, 26 insertions(+), 40 deletions(-) rename src/vs/workbench/contrib/void/browser/{inlineDiffsService.ts => editCodeService.ts} (98%) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts similarity index 98% rename from src/vs/workbench/contrib/void/browser/inlineDiffsService.ts rename to src/vs/workbench/contrib/void/browser/editCodeService.ts index f8449720..d8bae574 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -27,7 +27,7 @@ import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js'; -import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' +import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from './helpers/extractCodeFromResult.js'; @@ -215,7 +215,7 @@ type HistorySnapshot = { type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number } -export interface IInlineDiffsService { +export interface IEditCodeService { readonly _serviceBrand: undefined; startApplying(opts: StartApplyingOpts): number | undefined; interruptStreaming(diffareaid: number): void; @@ -224,9 +224,9 @@ export interface IInlineDiffsService { // testDiffs(): void; } -export const IInlineDiffsService = createDecorator('inlineDiffAreasService'); +export const IEditCodeService = createDecorator('editCodeService'); -class InlineDiffsService extends Disposable implements IInlineDiffsService { +class EditCodeService extends Disposable implements IEditCodeService { _serviceBrand: undefined; @@ -810,7 +810,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { type: UndoRedoElementType.Resource, resource: uri, label: 'Void Changes', - code: 'undoredo.inlineDiffs', + code: 'undoredo.editCode', undo: () => { restoreDiffAreas(beforeSnapshot) }, redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) } } @@ -1814,7 +1814,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } -registerSingleton(IInlineDiffsService, InlineDiffsService, InstantiationType.Eager); +registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager); const acceptBg = '#1a7431' const acceptAllBg = '#1e8538' @@ -2018,17 +2018,3 @@ class AcceptAllRejectAllWidget extends Widget implements IOverlayWidget { -// registerAction2(class extends Action2 { -// constructor() { -// super({ -// id: 'void.testDiff', -// title: localize2('voidTestDiff', 'Void Test Diff'), -// f1: true, -// }); -// } -// async run(accessor: ServicesAccessor): Promise { -// const inlineDiffsService = accessor.get(IInlineDiffsService) -// // inlineDiffsService.testDiffs() - -// } -// }) diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index 1a6e0deb..1099e74c 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -8,7 +8,7 @@ import { Action2, registerAction2 } from '../../../../platform/actions/common/ac import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { IInlineDiffsService } from './inlineDiffsService.js'; +import { IEditCodeService } from './editCodeService.js'; import { roundRangeToLines } from './sidebarActions.js'; import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js'; import { localize2 } from '../../../../nls.js'; @@ -63,7 +63,7 @@ registerAction2(class extends Action2 { const { startLineNumber: startLine, endLineNumber: endLine } = selection - const inlineDiffsService = accessor.get(IInlineDiffsService) - inlineDiffsService.addCtrlKZone({ startLine, endLine, editor }) + const editCodeService = accessor.get(IEditCodeService) + editCodeService.addCtrlKZone({ startLine, endLine, editor }) } }); diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 8f569074..ded3eaff 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -7,7 +7,7 @@ import React, { JSX, useCallback, useEffect, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js' -import { ChatMessageLocation, } from '../../../searchAndReplaceService.js' +import { ChatMessageLocation, } from '../../../aiRegexService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' @@ -33,7 +33,7 @@ const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, apply const accessor = useAccessor() const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) - const inlineDiffService = accessor.get('IInlineDiffsService') + const editCodeService = accessor.get('IEditCodeService') const clipboardService = accessor.get('IClipboardService') const metricsService = accessor.get('IMetricsService') @@ -56,13 +56,13 @@ const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, apply const onApply = useCallback(() => { - inlineDiffService.startApplying({ + editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr, }) metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only - }, [metricsService, inlineDiffService, applyStr]) + }, [metricsService, editCodeService, applyStr]) const isSingleLine = !applyStr.includes('\n') 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 57dbb472..f79c9af9 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 @@ -24,7 +24,7 @@ export const QuickEditChat = ({ }: QuickEditPropsType) => { const accessor = useAccessor() - const inlineDiffsService = accessor.get('IInlineDiffsService') + const editCodeService = accessor.get('IEditCodeService') const sizerRef = useRef(null) const textAreaRef = useRef(null) const textAreaFnsRef = useRef(null) @@ -57,26 +57,26 @@ export const QuickEditChat = ({ if (currStreamingDiffZoneRef.current !== null) return textAreaFnsRef.current?.disable() - const id = inlineDiffsService.startApplying({ + const id = editCodeService.startApplying({ from: 'QuickEdit', type:'rewrite', diffareaid: diffareaid, }) setCurrentlyStreamingDiffZone(id ?? null) - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid]) + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, editCodeService, diffareaid]) const onInterrupt = useCallback(() => { if (currStreamingDiffZoneRef.current === null) return - inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current) + editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) setCurrentlyStreamingDiffZone(null) textAreaFnsRef.current?.enable() - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService]) + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, editCodeService]) const onX = useCallback(() => { onInterrupt() - inlineDiffsService.removeCtrlKZone({ diffareaid }) - }, [inlineDiffsService, diffareaid]) + editCodeService.removeCtrlKZone({ diffareaid }) + }, [editCodeService, diffareaid]) useScrollbarStyles(sizerRef) 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 d8b4ef93..c658b10b 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 @@ -24,7 +24,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { Pencil, X } from 'lucide-react'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { ChatMessageLocation } from '../../../searchAndReplaceService.js'; +import { ChatMessageLocation } from '../../../aiRegexService.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 52af76f2..2d516ace 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -28,7 +28,7 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js'; import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'; -import { IInlineDiffsService } from '../../../inlineDiffsService.js'; +import { IEditCodeService } from '../../../editCodeService.js'; import { IVoidUriStateService } from '../../../voidUriStateService.js'; import { IQuickEditStateService } from '../../../quickEditStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; @@ -103,10 +103,10 @@ export const _registerServices = (accessor: ServicesAccessor) => { settingsStateService: accessor.get(IVoidSettingsService), refreshModelService: accessor.get(IRefreshModelService), themeService: accessor.get(IThemeService), - inlineDiffsService: accessor.get(IInlineDiffsService), + editCodeService: accessor.get(IEditCodeService), } - const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices + const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService } = stateServices uriState = uriStateService.state disposables.push( @@ -192,7 +192,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => { ILLMMessageService: accessor.get(ILLMMessageService), IRefreshModelService: accessor.get(IRefreshModelService), IVoidSettingsService: accessor.get(IVoidSettingsService), - IInlineDiffsService: accessor.get(IInlineDiffsService), + IEditCodeService: accessor.get(IEditCodeService), IVoidUriStateService: accessor.get(IVoidUriStateService), IQuickEditStateService: accessor.get(IQuickEditStateService), ISidebarStateService: accessor.get(ISidebarStateService), diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 19d20201..18fa1949 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -5,7 +5,7 @@ // register inline diffs -import './inlineDiffsService.js' +import './editCodeService.js' // register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L) import './sidebarActions.js' From 05d8b3a982a27d8a0ecb43743eaa23917c8a85cb Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Feb 2025 00:01:37 -0800 Subject: [PATCH 19/92] handle updates with version number instead of weird check --- .../contrib/void/browser/chatThreadService.ts | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index a3452eb2..ff20a6ec 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -154,64 +154,56 @@ class ChatThreadService extends Disposable implements IChatThreadService { ) { super() + const oldVersionNum = this._storageService.get(THREAD_VERSION_KEY, StorageScope.APPLICATION) + + + const readThreads = this._readAllThreads() + const updatedThreads = this._updatedThreadsToVersion(readThreads, oldVersionNum) + + if (updatedThreads !== null) { + this._storeAllThreads(updatedThreads) + } + + const allThreads = updatedThreads ?? readThreads this.state = { - allThreads: this._readAllThreads(), + allThreads: allThreads, currentThreadId: null as unknown as string, // gets set in startNewThread() } // always be in a thread this.openNewThread() - // for now just write the version, anticipating bigger changes in the future where we'll want to access this this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER) } private _readAllThreads(): ChatThreads { - // PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE - // CAN ADD "v0" TAG IN STORAGE AND CONVERT - - const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION) - const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {} - this._updateThreadsToVersion(threads, THREAD_VERSION) - return threads } - private _updateThreadsToVersion(oldThreadsObject: any, toVersion: string) { + // returns if should update + private _updatedThreadsToVersion(oldThreadsObject: any, oldVersion: string | undefined): ChatThreads | null { - if (toVersion === 'v2') { + if (!oldVersion) { - const threads: ChatThreads = oldThreadsObject + // unknown, just reset chat? + return null + } - /** v1 -> v2 - - threadsState.currentStagingSelections: CodeStagingSelection[] | null; - + thread.staging: StagingInfo - + thread.focusedMessageIdx?: number | undefined; - - + chatMessage.staging: StagingInfo | null - */ - - // check if we need to update - let shouldUpdate = false - for (const thread of Object.values(threads)) { - if (!thread.staging) { - shouldUpdate = true - } - for (const chatMessage of Object.values(thread.messages)) { - if (chatMessage.role === 'user' && !chatMessage.staging) { - shouldUpdate = true - } - } - } - - if (!shouldUpdate) return; + /** v1 -> v2 + - threadsState.currentStagingSelections: CodeStagingSelection[] | null; + + thread.staging: StagingInfo + + thread.focusedMessageIdx?: number | undefined; + + chatMessage.staging: StagingInfo | null + */ + else if (oldVersion === 'v1') { + const threads = oldThreadsObject as Omit // update the threads for (const thread of Object.values(threads)) { if (!thread.staging) { @@ -226,8 +218,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // push the update - this._storeAllThreads(threads) + return threads } + else if (oldVersion === 'v2') { + return null + } + + // up to date + return null } From bc6150aeac459b9371ae33b655b6a57b987c3f88 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Feb 2025 00:02:17 -0800 Subject: [PATCH 20/92] tools --- .../contrib/void/browser/editCodeService.ts | 40 +++++++++++++++- .../contrib/void/common/llmMessageTypes.ts | 2 +- .../contrib/void/common/toolsService.ts | 29 ++++++------ .../electron-main/llmMessage/anthropic.ts | 46 +++++++++---------- .../void/electron-main/llmMessage/gemini.ts | 2 +- .../void/electron-main/llmMessage/groq.ts | 2 +- .../void/electron-main/llmMessage/mistral.ts | 2 +- .../void/electron-main/llmMessage/ollama.ts | 7 ++- .../void/electron-main/llmMessage/openai.ts | 10 ++-- .../llmMessage/sendLLMMessage.ts | 16 +++---- .../void/electron-main/llmMessageChannel.ts | 2 +- 11 files changed, 97 insertions(+), 61 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index d8bae574..2db9cb90 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -42,6 +42,7 @@ import { ILLMMessageService } from '../common/llmMessageService.js'; import { LLMChatMessage, _InternalLLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { VSReadFile } from './helpers/readFile.js'; +import { voidTools } from '../common/toolsService.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -65,7 +66,6 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); - const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { const model = editor.getModel(); @@ -1138,6 +1138,44 @@ class EditCodeService extends Disposable implements IEditCodeService { + + + async startAgent(queryStr: string) { + // agent loop + const messages: LLMChatMessage[] = [] + + while (true) { + await new Promise((res, rej) => { + this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + tools: [voidTools['read_file']], + useProviderFor: 'Apply', + logging: { loggingName: `Agent` }, + messages, + onText: ({ fullText }) => { + + }, + onFinalMessage: async ({ fullText, tools }) => { + res(tools) + }, + onError: (e) => { + }, + }) + }) + } + + + + + } + + + stopAgent() { + + } + + + public startApplying(opts: StartApplyingOpts) { if (opts.type === 'rewrite') { diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 40ab1f2a..dcfd2c67 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -22,7 +22,7 @@ export const errorDetails = (fullError: Error | null): string | null => { } export type OnText = (p: { newText: string, fullText: string }) => void -export type OnFinalMessage = (p: { fullText: string }) => void +export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string }[] }) => void export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 86cc4356..43b18cb8 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -29,7 +29,7 @@ const paginationHelper = { param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } } as const -export const contextTools = { +export const voidTools: { [name: string]: InternalToolInfo } = { read_file: { name: 'read_file', description: 'Returns file contents of a given URI.', @@ -73,12 +73,11 @@ export const contextTools = { // description: 'Searches files semantically for the given string query.', // // RAG // }, - } -export type ContextToolName = keyof typeof contextTools -type ContextToolParamNames = keyof typeof contextTools[T]['params'] -type ContextToolParams = { [paramName in ContextToolParamNames]: unknown } +export type ToolName = keyof typeof voidTools +type ToolParamNames = keyof typeof voidTools[T]['params'] +type ToolParamsObj = { [paramName in ToolParamNames]: unknown } @@ -121,7 +120,7 @@ export class ToolService implements IToolService { readonly _serviceBrand: undefined; - public contextToolCallFns + public toolFns constructor( @IFileService fileService: IFileService, @@ -133,32 +132,32 @@ export class ToolService implements IToolService { const queryBuilder = instantiationService.createInstance(QueryBuilder); - this.contextToolCallFns = { - read_file: async ({ uri: uriStr }: ContextToolParams<'read_file'>) => { + this.toolFns = { + read_file: async ({ uri: uriStr }: ToolParamsObj<'read_file'>) => { const uri = validateURI(uriStr) const fileContents = await VSReadFileRaw(fileService, uri) return fileContents ?? '(could not read file)' }, - list_dir: async ({ uri: uriStr }: ContextToolParams<'list_dir'>) => { + list_dir: async ({ uri: uriStr }: ToolParamsObj<'list_dir'>) => { const uri = validateURI(uriStr) // TODO!!!! check to make sure in workspace // TODO check to make sure is not gitignored const treeStr = await generateDirectoryTreeMd(fileService, uri) return treeStr }, - pathname_search: async ({ query: queryStr }: ContextToolParams<'pathname_search'>) => { + pathname_search: async ({ query: queryStr }: ToolParamsObj<'pathname_search'>) => { if (typeof queryStr !== 'string') return '(Error: query was not a string)' - const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }); + const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }) - const data = await searchService.fileSearch(query, CancellationToken.None); + const data = await searchService.fileSearch(query, CancellationToken.None) const URIs = data.results.map(({ resource, results }) => resource.fsPath) return URIs }, - search: async ({ query: queryStr }: ContextToolParams<'search'>) => { + search: async ({ query: queryStr }: ToolParamsObj<'search'>) => { if (typeof queryStr !== 'string') return '(Error: query was not a string)' - const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)); + const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)) - const data = await searchService.textSearch(query, CancellationToken.None); + const data = await searchService.textSearch(query, CancellationToken.None) const URIs = data.results.map(({ resource, results }) => resource) return URIs }, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index e957c83a..75ae7d89 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -6,7 +6,7 @@ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; -import { InternalToolInfo } from '../../common/toolsService.js'; +import { InternalToolInfo, voidTools } from '../../common/toolsService.js'; @@ -45,7 +45,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, messages: messages, model: modelName, max_tokens: maxTokens, - // tools: [toAnthropicTool(contextTools.list_dir)] + tools: [toAnthropicTool(voidTools.list_dir)] }); @@ -55,33 +55,31 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, }) - // can do tool use streaming - const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} - stream.on('streamEvent', e => { - if (e.type === 'content_block_start') { - if (e.content_block.type !== 'tool_use') return - const index = e.index - if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' } - toolCallOfIndex[index].name += e.content_block.name ?? '' - toolCallOfIndex[index].args += e.content_block.input ?? '' - } - else if (e.type === 'content_block_delta') { - if (e.delta.type !== 'input_json_delta') return - toolCallOfIndex[e.index].args += e.delta.partial_json - } - // TODO!!!!! - // onText({}) - }) + // // can do tool use streaming + // const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} + // stream.on('streamEvent', e => { + // if (e.type === 'content_block_start') { + // if (e.content_block.type !== 'tool_use') return + // const index = e.index + // if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' } + // toolCallOfIndex[index].name += e.content_block.name ?? '' + // toolCallOfIndex[index].args += e.content_block.input ?? '' + // } + // else if (e.type === 'content_block_delta') { + // if (e.delta.type !== 'input_json_delta') return + // toolCallOfIndex[e.index].args += e.delta.partial_json + // } + // // TODO!!!!! + // // onText({}) + // }) // when we get the final message on this stream (or when error/fail) stream.on('finalMessage', (response) => { // stringify the response's content - const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n') - const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, input: c.input } : null).filter(c => !!c) + const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') + const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input) } : null).filter(c => !!c) - console.log("TOOLS!!!!", typeof tools[0]?.input, JSON.stringify(tools, null, 2)) - - onFinalMessage({ fullText: content, }) + onFinalMessage({ fullText: content, tools }) }) stream.on('error', (error) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/gemini.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/gemini.ts index eef8cc3a..2732fbe4 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/gemini.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/gemini.ts @@ -35,7 +35,7 @@ export const sendGeminiChat: _InternalSendLLMChatMessageFnType = async ({ messag fullText += newText; onText({ newText, fullText }); } - onFinalMessage({ fullText }); + onFinalMessage({ fullText, tools: [] }); }) .catch((error) => { onError({ message: error + '', fullError: error }) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts index 8f7efd14..c6fcb290 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts @@ -32,7 +32,7 @@ export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages onText({ newText, fullText }); } - onFinalMessage({ fullText }); + onFinalMessage({ fullText, tools: [] }); }) .catch(error => { onError({ message: error + '', fullError: error }); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts index cfddc2a5..ea3179ed 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts @@ -36,7 +36,7 @@ export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messa onText({ newText, fullText }); } - onFinalMessage({ fullText }); + onFinalMessage({ fullText, tools: [] }); }) .catch(error => { onError({ message: error + '', fullError: error }); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts index 5a69698b..daff3a29 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts @@ -68,7 +68,7 @@ export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe fullText += newText; onText({ newText, fullText }); } - onFinalMessage({ fullText }); + onFinalMessage({ fullText, tools: [] }); }) // when error/fail .catch((error) => { @@ -100,14 +100,13 @@ export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, on for await (const chunk of stream) { const newText = chunk.message.content; - - // chunk.message.tool_calls[0].function.arguments fullText += newText; onText({ newText, fullText }); } - onFinalMessage({ fullText }); + + onFinalMessage({ fullText, tools: [] }); }) // when error/fail diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index ead592d3..4744db62 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -111,30 +111,32 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe // openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models - onFinalMessage({ fullText: 'TODO' }) + } // OpenAI, OpenRouter, OpenAICompatible -export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => { +export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }) => { let fullText = '' const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} + const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, - // tools: Object.keys(contextTools).map(name => toOpenAITool(name, contextTools[name as ContextToolName])), + tools: tools?.map(tool => toOpenAITool(tool)), } openai.chat.completions .create(options) .then(async response => { _setAborter(() => response.controller.abort()) + // when receive text for await (const chunk of response) { @@ -153,7 +155,7 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on onText({ newText, fullText }); } - onFinalMessage({ fullText }); + onFinalMessage({ fullText, tools: Object.keys(toolCallOfIndex).map(index => toolCallOfIndex[index]) }); }) // when error/fail - this catches errors of both .create() and .then(for await) .catch(error => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 86a77362..6ebdbdf6 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -8,7 +8,7 @@ import { IMetricsService } from '../../common/metricsService.js'; import { sendAnthropicChat } from './anthropic.js'; import { sendOllamaFIM, sendOllamaChat } from './ollama.js'; -import { sendOpenAIChat, sendOpenAIFIM } from './openai.js'; +import { sendOpenAIChat } from './openai.js'; import { sendGeminiChat } from './gemini.js'; import { sendGroqChat } from './groq.js'; import { sendMistralChat } from './mistral.js'; @@ -107,10 +107,10 @@ export const sendLLMMessage = ({ _fullTextSoFar = fullText } - const onFinalMessage: OnFinalMessage = ({ fullText }) => { + const onFinalMessage: OnFinalMessage = ({ fullText, tools }) => { if (_didAbort) return captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) - onFinalMessage_({ fullText }) + onFinalMessage_({ fullText, tools }) } const onError: OnError = ({ message: error, fullError }) => { @@ -141,7 +141,7 @@ export const sendLLMMessage = ({ case 'openRouter': case 'deepseek': case 'openAICompatible': - if (messagesType === 'FIMMessage') sendOpenAIFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] }) else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'ollama': @@ -149,19 +149,19 @@ export const sendLLMMessage = ({ else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'anthropic': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', tools: [] }) else /* */ sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'gemini': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM' }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM', tools: [] }) else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'groq': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM' }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM', tools: [] }) else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'mistral': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM' }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM', tools: [] }) else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; default: diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts index 98725631..9db9a68f 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts @@ -100,7 +100,7 @@ export class LLMMessageChannel implements IServerChannel { const mainThreadParams: SendLLMMessageParams = { ...params, onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); }, - onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); }, + onFinalMessage: ({ fullText, tools }) => { this._onFinalMessage_llm.fire({ requestId, fullText, tools }); }, onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); }, abortRef: this._abortRefOfRequestId_llm[requestId], } From 152e605856a93e22557d0cb7cba4786dc67c807d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Feb 2025 02:05:37 -0800 Subject: [PATCH 21/92] tool progress! --- .../contrib/void/browser/chatThreadService.ts | 169 +++++++++++++----- .../contrib/void/browser/editCodeService.ts | 35 ---- .../contrib/void/browser/helpers/readFile.ts | 10 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 10 +- .../contrib/void/browser/sidebarActions.ts | 2 +- .../contrib/void/common/llmMessageTypes.ts | 2 +- .../contrib/void/common/toolsService.ts | 92 ++++++++-- .../electron-main/llmMessage/anthropic.ts | 8 +- 8 files changed, 215 insertions(+), 113 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index ff20a6ec..0c5b761a 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -14,6 +14,7 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; +import { IToolsService, ToolName, voidTools } from '../common/toolsService.js'; // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { @@ -60,6 +61,13 @@ export type ChatMessage = content: string; displayContent?: undefined; } + | { + role: 'tool'; + name: string; // internal use + params: string | null; // internal use + content: string | null; // summary of the tool to the LLM + displayContent: string | null; // text message of result + } // a 'thread' means a chat message history export type ChatThreads = { @@ -124,7 +132,7 @@ export interface IChatThreadService { isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; - _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; + useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise; addUserMessageAndStreamResponse(userMessage: string): Promise; @@ -151,6 +159,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IStorageService private readonly _storageService: IStorageService, @IModelService private readonly _modelService: IModelService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, + @IToolsService private readonly _toolsService: IToolsService, ) { super() @@ -254,14 +263,120 @@ class ChatThreadService extends Disposable implements IChatThreadService { // ---------- streaming ---------- - finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => { + private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => { // add assistant's message to chat history, and clear selection - const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null } - this._addMessageToThread(threadId, assistantHistoryElt) + this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null }) this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error }) } + + + async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { + + const thread = this.getCurrentThread() + const threadId = thread.id + + let threadStaging = thread.staging + + const currStaging = stagingOverride ?? threadStaging ?? defaultStaging // don't use _useFocusedStagingState to avoid race conditions with focusing + const { selections: currSelns, } = currStaging + + // add user's message to chat history + const instructions = userMessage + const content = await chat_userMessage(instructions, currSelns, this._modelService) + const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, staging: null, } + this._addMessageToThread(threadId, userHistoryElt) + + this._setStreamState(threadId, { error: undefined }) + + + + // agent loop + + + let shouldContinue = false + do { + shouldContinue = false + + console.log('Q') + + let res_: () => void + const awaitable = new Promise((res, rej) => { res_ = res }) + + const llmCancelToken = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: 'Ctrl+L', + logging: { loggingName: `Agent` }, + messages: [ + { role: 'system', content: chat_systemMessage }, + ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), + ], + tools: [voidTools['read_file']], + + onText: ({ fullText }) => { + this._setStreamState(threadId, { messageSoFar: fullText }) + }, + onFinalMessage: async ({ fullText, tools }) => { + if (tools.length === 0) { + this._finishStreamingTextMessage(threadId, fullText) + } + else { + for (const tool of tools) { + if (!(tool.name in this._toolsService.toolFns)) { + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) + } + else { + const toolName = tool.name as ToolName + const toolResult = await this._toolsService.toolFns[toolName](JSON.parse(tool.args)) + const string = this._toolsService.toolResultToString[toolName](toolResult as any) + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: string, displayContent: string, }) + shouldContinue = true + } + } + } + res_() + }, + onError: (error) => { + this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) + res_() + }, + }) + if (llmCancelToken === null) return + this._setStreamState(threadId, { streamingToken: llmCancelToken }) + + await awaitable + } + while (shouldContinue); + + + + + // const llmCancelToken = this._llmMessageService.sendLLMMessage({ + // messagesType: 'chatMessages', + // logging: { loggingName: 'Chat' }, + // useProviderFor: 'Ctrl+L', + // messages: [ + // { role: 'system', content: chat_systemMessage }, + // ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), + // ], + // onText: ({ newText, fullText }) => { + // this._setStreamState(threadId, { messageSoFar: fullText }) + // }, + // onFinalMessage: ({ fullText: content }) => { + // this._finishStreaming(threadId, content) + // }, + // onError: (error) => { + // this._finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) + // }, + + // }) + // if (llmCancelToken === null) return + // this._setStreamState(threadId, { streamingToken: llmCancelToken }) + + } + + async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) { const thread = this.getCurrentThread() @@ -284,58 +399,18 @@ class ChatThreadService extends Disposable implements IChatThreadService { } }, true) - // stream the edit + // re-add the message and stream it this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging) } - async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { - const thread = this.getCurrentThread() - const threadId = thread.id - - let threadStaging = thread.staging - - const currStaging = stagingOverride ?? threadStaging ?? defaultStaging // don't use _useFocusedStagingState to avoid race conditions with focusing - const { selections: currSelns, } = currStaging - - // add user's message to chat history - const instructions = userMessage - const content = await chat_userMessage(instructions, currSelns, this._modelService) - const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, staging: null, } - this._addMessageToThread(threadId, userHistoryElt) - - this._setStreamState(threadId, { error: undefined }) - - const llmCancelToken = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - logging: { loggingName: 'Chat' }, - useProviderFor: 'Ctrl+L', - messages: [ - { role: 'system', content: chat_systemMessage }, - ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), - ], - onText: ({ newText, fullText }) => { - this._setStreamState(threadId, { messageSoFar: fullText }) - }, - onFinalMessage: ({ fullText: content }) => { - this.finishStreaming(threadId, content) - }, - onError: (error) => { - this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) - }, - - }) - if (llmCancelToken === null) return - this._setStreamState(threadId, { streamingToken: llmCancelToken }) - - } cancelStreaming(threadId: string) { const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '') + this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '') } dismissStreamError(threadId: string): void { @@ -475,7 +550,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) - _useFocusedStagingState(messageIdx?: number | undefined) { + useFocusedStagingState(messageIdx?: number | undefined) { const defaultStaging = { isBeingEdited: false, selections: [], text: '' } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2db9cb90..52af46c2 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -42,7 +42,6 @@ import { ILLMMessageService } from '../common/llmMessageService.js'; import { LLMChatMessage, _InternalLLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { VSReadFile } from './helpers/readFile.js'; -import { voidTools } from '../common/toolsService.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -1140,40 +1139,6 @@ class EditCodeService extends Disposable implements IEditCodeService { - async startAgent(queryStr: string) { - // agent loop - const messages: LLMChatMessage[] = [] - - while (true) { - await new Promise((res, rej) => { - this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - tools: [voidTools['read_file']], - useProviderFor: 'Apply', - logging: { loggingName: `Agent` }, - messages, - onText: ({ fullText }) => { - - }, - onFinalMessage: async ({ fullText, tools }) => { - res(tools) - }, - onError: (e) => { - }, - }) - }) - } - - - - - } - - - stopAgent() { - - } - public startApplying(opts: StartApplyingOpts) { diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts index b0f154d1..39cd310d 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -11,7 +11,11 @@ export const VSReadFile = async (modelService: IModelService, uri: URI): Promise } export const VSReadFileRaw = async (fileService: IFileService, uri: URI) => { - const res = await fileService.readFile(uri) - const str = res.value.toString() - return str + try { + const res = await fileService.readFile(uri) + const str = res.value.toString() + return str + } catch (e) { + return 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 c658b10b..73118cf8 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 @@ -551,7 +551,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM const chatThreadsService = accessor.get('IChatThreadService') // edit mode state - const [staging, setStaging] = chatThreadsService._useFocusedStagingState(messageIdx) + const [staging, setStaging] = chatThreadsService.useFocusedStagingState(messageIdx) const mode: ChatBubbleMode = staging.isBeingEdited ? 'edit' : 'display' const [isFocused, setIsFocused] = useState(false) const [isHovered, setIsHovered] = useState(false) @@ -682,6 +682,9 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatbubbleContents = } + else if (role === 'tool'){ + chatbubbleContents = chatMessage.name + } return
{ const currentThread = chatThreadsService.getCurrentThread() const previousMessages = currentThread?.messages ?? [] - const [staging, setStaging] = chatThreadsService._useFocusedStagingState() + const [staging, setStaging] = chatThreadsService.useFocusedStagingState() // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) @@ -822,7 +825,7 @@ export const SidebarChat = () => { const prevMessagesHTML = useMemo(() => { return previousMessages.map((message, i) => - + ) }, [previousMessages]) @@ -836,6 +839,7 @@ export const SidebarChat = () => { const messagesHTML = setStaging({ ...staging, selections: s }) diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index dcfd2c67..f6ea2a2f 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -27,7 +27,7 @@ export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } export type LLMChatMessage = { - role: 'system' | 'user' | 'assistant'; + role: 'system' | 'user' | 'assistant' | 'tool'; content: string; } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 43b18cb8..32004ff2 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -29,7 +29,7 @@ const paginationHelper = { param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } } as const -export const voidTools: { [name: string]: InternalToolInfo } = { +export const voidTools = { read_file: { name: 'read_file', description: 'Returns file contents of a given URI.', @@ -73,16 +73,22 @@ export const voidTools: { [name: string]: InternalToolInfo } = { // description: 'Searches files semantically for the given string query.', // // RAG // }, -} +} satisfies { [name: string]: InternalToolInfo } export type ToolName = keyof typeof voidTools -type ToolParamNames = keyof typeof voidTools[T]['params'] -type ToolParamsObj = { [paramName in ToolParamNames]: unknown } - - +export type ToolParamNames = keyof typeof voidTools[T]['params'] +export type ToolParamsObj = { [paramName in ToolParamNames]: unknown } +export type ToolCallReturnType + = T extends 'read_file' ? Promise + : T extends 'list_dir' ? Promise + : T extends 'pathname_search' ? Promise + : T extends 'search' ? Promise + : never +export type ToolFns = { [T in ToolName]: (p: string) => ToolCallReturnType } +export type ToolResultToString = { [T in ToolName]: (result: Awaited>) => string } async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise { @@ -110,17 +116,21 @@ const validateURI = (uriStr: unknown) => { return uri } -export interface IToolService { +export interface IToolsService { readonly _serviceBrand: undefined; + toolFns: ToolFns; + toolResultToString: ToolResultToString; } -export const IToolService = createDecorator('ToolService'); +export const IToolsService = createDecorator('ToolsService'); -export class ToolService implements IToolService { +export class ToolsService implements IToolsService { readonly _serviceBrand: undefined; - public toolFns + public toolFns: ToolFns + public toolResultToString: ToolResultToString + constructor( @IFileService fileService: IFileService, @@ -132,29 +142,56 @@ export class ToolService implements IToolService { const queryBuilder = instantiationService.createInstance(QueryBuilder); + const parseObj = (s: string): { [s: string]: unknown } | null => { + try { + const o = JSON.parse(s) + return o + } + catch (e) { + return null + } + } + + const invalidToolParamMsg = '(LLM parameter format was invalid for this tool)' this.toolFns = { - read_file: async ({ uri: uriStr }: ToolParamsObj<'read_file'>) => { + read_file: async (s: string) => { + const o = parseObj(s) + if (!o) return invalidToolParamMsg + const { uri: uriStr } = o + const uri = validateURI(uriStr) const fileContents = await VSReadFileRaw(fileService, uri) - return fileContents ?? '(could not read file)' + return fileContents ?? invalidToolParamMsg }, - list_dir: async ({ uri: uriStr }: ToolParamsObj<'list_dir'>) => { + list_dir: async (s: string) => { + const o = parseObj(s) + if (!o) return invalidToolParamMsg + const { uri: uriStr } = o + const uri = validateURI(uriStr) // TODO!!!! check to make sure in workspace // TODO check to make sure is not gitignored const treeStr = await generateDirectoryTreeMd(fileService, uri) return treeStr }, - pathname_search: async ({ query: queryStr }: ToolParamsObj<'pathname_search'>) => { - if (typeof queryStr !== 'string') return '(Error: query was not a string)' + pathname_search: async (s: string) => { + const o = parseObj(s) + if (!o) return invalidToolParamMsg + const { query: queryStr } = o + + if (typeof queryStr !== 'string') return 'Error: query was not a string' const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }) const data = await searchService.fileSearch(query, CancellationToken.None) - const URIs = data.results.map(({ resource, results }) => resource.fsPath) + const URIs = data.results.map(({ resource, results }) => resource) return URIs }, - search: async ({ query: queryStr }: ToolParamsObj<'search'>) => { - if (typeof queryStr !== 'string') return '(Error: query was not a string)' + search: async (s: string) => { + const o = parseObj(s) + if (!o) return '(could not search)' + const { query: queryStr } = o + + if (typeof queryStr !== 'string') return 'Error: query was not a string' const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)) const data = await searchService.textSearch(query, CancellationToken.None) @@ -164,6 +201,23 @@ export class ToolService implements IToolService { } + this.toolResultToString = { + read_file: (URIs) => { + return URIs + }, + list_dir: (URIs) => { + return URIs + }, + pathname_search: (URIs) => { + if (typeof URIs === 'string') return URIs + return URIs.map(uri => uri.fsPath).join('\n') + }, + search: (URIs) => { + if (typeof URIs === 'string') return URIs + return URIs.map(uri => uri.fsPath).join('\n') + }, + } + } @@ -171,5 +225,5 @@ export class ToolService implements IToolService { } -registerSingleton(IToolService, ToolService, InstantiationType.Eager); +registerSingleton(IToolsService, ToolsService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index 75ae7d89..aefe6c34 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -6,7 +6,7 @@ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; -import { InternalToolInfo, voidTools } from '../../common/toolsService.js'; +import { InternalToolInfo } from '../../common/toolsService.js'; @@ -28,7 +28,7 @@ export const toAnthropicTool = (toolInfo: InternalToolInfo) => { -export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, tools }) => { const thisConfig = settingsOfProvider.anthropic @@ -45,8 +45,8 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, messages: messages, model: modelName, max_tokens: maxTokens, - tools: [toAnthropicTool(voidTools.list_dir)] - }); + tools: tools?.map(tool => toAnthropicTool(tool)) + }) // when receive text From 8591d06244fc936a8110fe734a3d9d0f9c53fcba Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Feb 2025 19:23:15 -0800 Subject: [PATCH 22/92] tool use plugboard progress --- .../contrib/void/browser/chatThreadService.ts | 33 ++- .../contrib/void/browser/editCodeService.ts | 6 +- .../contrib/void/browser/prompt/prompts.ts | 6 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 6 +- .../contrib/void/common/llmMessageTypes.ts | 17 +- .../void/common/voidSettingsService.ts | 4 +- .../contrib/void/common/voidSettingsTypes.ts | 238 ++++++++++-------- .../electron-main/llmMessage/anthropic.ts | 5 +- .../void/electron-main/llmMessage/groq.ts | 42 ---- .../void/electron-main/llmMessage/mistral.ts | 44 ---- .../void/electron-main/llmMessage/openai.ts | 14 +- .../llmMessage/sendLLMMessage.ts | 206 ++++++++++++--- 12 files changed, 367 insertions(+), 254 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 0c5b761a..98165ce0 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -65,6 +65,7 @@ export type ChatMessage = role: 'tool'; name: string; // internal use params: string | null; // internal use + tool_use_id: string; // apis require this content: string | null; // summary of the tool to the LLM displayContent: string | null; // text message of result } @@ -111,10 +112,12 @@ const newThreadObject = () => { } const THREAD_VERSION_KEY = 'void.chatThreadVersion' -const THREAD_VERSION = 'v2' +const LATEST_THREAD_VERSION = 'v2' const THREAD_STORAGE_KEY = 'void.chatThreadStorage' + +type ChatMode = 'agent' | 'chat' export interface IChatThreadService { readonly _serviceBrand: undefined; @@ -134,8 +137,8 @@ export interface IChatThreadService { useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; - editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise; - addUserMessageAndStreamResponse(userMessage: string): Promise; + editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; + addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise; cancelStreaming(threadId: string): void; dismissStreamError(threadId: string): void; @@ -182,7 +185,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // always be in a thread this.openNewThread() - this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER) + this._storageService.store(THREAD_VERSION_KEY, LATEST_THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER) } @@ -272,7 +275,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { - async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { + async addUserMessageAndStreamResponse({ userMessage, chatMode, stagingOverride }: { userMessage: string, chatMode: ChatMode, stagingOverride?: StagingInfo | null }) { const thread = this.getCurrentThread() const threadId = thread.id @@ -293,14 +296,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { // agent loop - - let shouldContinue = false do { shouldContinue = false - console.log('Q') - let res_: () => void const awaitable = new Promise((res, rej) => { res_ = res }) @@ -310,9 +309,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { logging: { loggingName: `Agent` }, messages: [ { role: 'system', content: chat_systemMessage }, - ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), + ...this.getCurrentThread().messages.map(m => ({ ...m, content: m.content || '(empty model output)' })), ], - tools: [voidTools['read_file']], + tools: [voidTools['read_file']], // TODO!!!!! make this change on agent | chat | search onText: ({ fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) @@ -324,13 +323,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { else { for (const tool of tools) { if (!(tool.name in this._toolsService.toolFns)) { - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, tool_use_id: tool.tool_use_id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) } else { const toolName = tool.name as ToolName - const toolResult = await this._toolsService.toolFns[toolName](JSON.parse(tool.args)) - const string = this._toolsService.toolResultToString[toolName](toolResult as any) - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: string, displayContent: string, }) + const toolResult = await this._toolsService.toolFns[toolName](tool.args) + const string = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, tool_use_id: tool.tool_use_id, content: string, displayContent: string, }) shouldContinue = true } } @@ -377,7 +376,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) { + async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) { const thread = this.getCurrentThread() @@ -400,7 +399,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, true) // re-add the message and stream it - this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging) + this.addUserMessageAndStreamResponse({ userMessage, chatMode, stagingOverride: messageToReplace.staging }) } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 52af46c2..36173d68 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, rewriteCode_userMessage, rewriteCode_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -1415,9 +1415,9 @@ class EditCodeService extends Disposable implements IEditCodeService { let messages: LLMChatMessage[] if (from === 'ClickApply') { - const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, uri }) messages = [ - { role: 'system', content: fastApply_rewritewholething_systemMessage, }, + { role: 'system', content: rewriteCode_systemMessage, }, { role: 'user', content: userContent, } ] } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 45a573ae..415a0c87 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -187,7 +187,7 @@ export const chat_userMessage = async (instructions: string, selections: Staging -export const fastApply_rewritewholething_systemMessage = `\ +export const rewriteCode_systemMessage = `\ You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`. Directions: @@ -199,7 +199,7 @@ Directions: -export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { +export const rewriteCode_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' @@ -311,7 +311,7 @@ Directions: 4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. - Make sure you add all necessary imports. - Make sure the "final" code is complete and will not result in syntax/lint errors. -5. Follow coding convention (spaces, semilcolons, comments, etc). +5. Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise. ## EXAMPLE 1 ORIGINAL_FILE 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 73118cf8..4f337a60 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 @@ -619,7 +619,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM // stream the edit const userMessage = textAreaRefState.value; - await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx) + await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx }) } const onAbort = () => { @@ -682,7 +682,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatbubbleContents = } - else if (role === 'tool'){ + else if (role === 'tool') { chatbubbleContents = chatMessage.name } @@ -798,7 +798,7 @@ export const SidebarChat = () => { // send message to LLM const userMessage = textAreaRef.current?.value ?? '' - await chatThreadsService.addUserMessageAndStreamResponse(userMessage) + await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' }) setStaging({ ...staging, selections: [], }) // clear staging textAreaFnsRef.current?.setValue('') diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index f6ea2a2f..27ce34c1 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -22,17 +22,28 @@ export const errorDetails = (fullError: Error | null): string | null => { } export type OnText = (p: { newText: string, fullText: string }) => void -export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string }[] }) => void +export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string, tool_use_id: string, }[] }) => void export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } export type LLMChatMessage = { - role: 'system' | 'user' | 'assistant' | 'tool'; + role: 'system' | 'user'; + content: string; +} | { + role: 'tool'; + tool_use_id: string; + content: string; +} | { + role: 'assistant', + tool_calls?: { name: string, tool_use_id: string, params: string }[]; content: string; } + + export type _InternalLLMChatMessage = { - role: 'user' | 'assistant'; + role: any; + tool_use_id?: any; content: string; } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index e44a294a..af68ea38 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IMetricsService } from './metricsService.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings, developerInfoOfRecognizedModel, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; const STORAGE_KEY = 'void.settingsServiceStorage' @@ -334,7 +334,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { if (existingIdx !== -1) return // if exists, do nothing const newModels = [ ...models, - { ...developerInfoOfRecognizedModel(modelName), modelName, isDefault: false, isHidden: false } + { ...developerInfoOfModelName(modelName), modelName, isDefault: false, isHidden: false } ] this.setSettingOfProvider(providerName, 'models', newModels) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 5d371b5d..da5aa81b 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -9,17 +9,25 @@ import { VoidSettingsState } from './voidSettingsService.js' // developer info used in sendLLMMessage -export type VoidModelDeveloperInfo = { +export type DeveloperInfoAtModel = { // USED: + // TODO!!! think tokens - deepseek + // TODO!!!! // UNUSED (coming soon): - recognizedModelName: RecognizedModel, // used to show user if model was auto-recognized + recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized supportsTools: boolean, // we will just do a string of tool use if it doesn't support - supportsSystemMessage: 'system' | 'developer' | false, // if null, we will just do a string of system message + supportsSystemMessage: 'developer' | 'system' | false, // if null, we will just do a string of system message supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> supportsStreaming: boolean, // (o1 does NOT) we will just dump the final result if doesn't support it - maxTokens: number, // required, DEFAULT is Infinity + maxTokens: number, // required +} + +export type DeveloperInfoAtProvider = { + separateSystemMessage?: boolean; + toolsGoInRole?: boolean; // whether to do {role:'tool'} or {role:'user' tool:...} + modelOverrides?: Partial; // any overrides for models that a provider might have (e.g. if a provider always supports tool use, even if we don't recognize the model we can set tools to true) } @@ -31,7 +39,7 @@ export type VoidModelInfo = { // <-- STATEFUL isDefault: boolean, // whether or not it's a default for its provider isHidden: boolean, // whether or not the user is hiding it (switched off) isAutodetected?: boolean, // whether the model was autodetected by polling -} & VoidModelDeveloperInfo +} & DeveloperInfoAtModel @@ -62,131 +70,155 @@ export const recognizedModels = [ ] as const +type RecognizedModelName = (typeof recognizedModels)[number] | '' - -type RecognizedModel = (typeof recognizedModels)[number] | '' - - -// const modelCapabilities: { [recognizedModel in RecognizedModel]: ({ }) => string } = { -// 'OpenAI 4o': { -// template: ({ prefix, suffix, }: { prefix: string; suffix: string; }) => `\ -// ` -// } -// } - -export function getRecognizedModel(modelName: string): RecognizedModel { +export function recognizedModelOfModelName(modelName: string): RecognizedModelName { const lower = modelName.toLowerCase(); - if (lower.includes('gpt-4o')) { + if (lower.includes('gpt-4o')) return 'OpenAI 4o'; - } - if (lower.includes('claude')) { + if (lower.includes('claude')) return 'Anthropic Claude'; - } - if (lower.includes('llama')) { + if (lower.includes('llama')) return 'Llama 3.x'; - } - if (lower.includes('qwen2.5-coder')) { + if (lower.includes('qwen2.5-coder')) return 'Alibaba Qwen2.5 Coder Instruct'; - } - if (lower.includes('mistral')) { + if (lower.includes('mistral')) return 'Mistral Codestral'; - } - // Check for "o1" or "o3" - if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) { + if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 return 'OpenAI o1, o3'; - } - if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) { + if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return 'Deepseek R1'; - } + if (lower.includes('deepseek')) + return 'Deepseek Chat' - // Fallback: return ''; } +const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { + 'anthropic': { + separateSystemMessage: true, + toolsGoInRole: false, + modelOverrides: { + supportsTools: true, + } + }, + 'deepseek': { + separateSystemMessage: true, + }, + 'openAI': { + separateSystemMessage: false, + toolsGoInRole: true, + }, + 'gemini': { + separateSystemMessage: true, + toolsGoInRole: false + }, + 'mistral': { + separateSystemMessage: true, + }, + 'groq': { + separateSystemMessage: true, + }, + 'ollama': { + separateSystemMessage: false, + }, + 'openRouter': { + separateSystemMessage: true, + }, + 'openAICompatible': { + separateSystemMessage: true, + }, +} +export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { + return developerInfoAtProvider[providerName] ?? {} +} -export const developerInfoOfRecognizedModel = (modelName: string) => { - const devInfo: { [recognizedModel in RecognizedModel]: Omit } = { - 'OpenAI 4o': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, - 'Anthropic Claude': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, - 'Llama 3.x': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, - 'Deepseek Chat': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, +// providerName is optional, but gives some extra fallbacks if provided +const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { + 'OpenAI 4o': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - 'Alibaba Qwen2.5 Coder Instruct': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, + 'Anthropic Claude': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - 'Mistral Codestral': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, + 'Llama 3.x': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - 'OpenAI o1, o3': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, + 'Deepseek Chat': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - 'Deepseek R1': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, + 'Alibaba Qwen2.5 Coder Instruct': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - '': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, - } + 'Mistral Codestral': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - const recognizedModelName = getRecognizedModel(modelName) + 'OpenAI o1, o3': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + 'Deepseek R1': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + '': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, +} +export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { + const recognizedModelName = recognizedModelOfModelName(modelName) return { recognizedModelName: recognizedModelName, - ...devInfo[recognizedModelName], + ...developerInfoOfRecognizedModelName[recognizedModelName], + ...overrides } } @@ -202,7 +234,7 @@ export const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): VoidM isDefault: true, isAutodetected: false, isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually - ...developerInfoOfRecognizedModel(modelName), + ...developerInfoOfModelName(modelName), })) } @@ -219,7 +251,7 @@ export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], o isDefault: true, isAutodetected: true, isHidden: !!existingModelsMap[modelName]?.isHidden, - ...developerInfoOfRecognizedModel(modelName) + ...developerInfoOfModelName(modelName) })) } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index aefe6c34..b443cca1 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -45,7 +45,8 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, messages: messages, model: modelName, max_tokens: maxTokens, - tools: tools?.map(tool => toAnthropicTool(tool)) + tools: tools?.map(tool => toAnthropicTool(tool)), + tool_choice: { type: 'auto', disable_parallel_tool_use: true } // one tool use at a time }) @@ -77,7 +78,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, stream.on('finalMessage', (response) => { // stringify the response's content const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input) } : null).filter(c => !!c) + const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input), tool_use_id: c.id } : null).filter(c => !!c) onFinalMessage({ fullText: content, tools }) }) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts deleted file mode 100644 index c6fcb290..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import Groq from 'groq-sdk'; -import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// Groq -export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - let fullText = ''; - - const thisConfig = settingsOfProvider.groq - - const groq = new Groq({ - apiKey: thisConfig.apiKey, - dangerouslyAllowBrowser: true - }); - - await groq.chat.completions - .create({ - messages: messages, - model: modelName, - stream: true, - }) - .then(async response => { - _setAborter(() => response.controller.abort()) - // when receive text - for await (const chunk of response) { - const newText = chunk.choices[0]?.delta?.content || ''; - fullText += newText; - onText({ newText, fullText }); - } - - onFinalMessage({ fullText, tools: [] }); - }) - .catch(error => { - onError({ message: error + '', fullError: error }); - }) - - -}; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts deleted file mode 100644 index ea3179ed..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts +++ /dev/null @@ -1,44 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Mistral } from '@mistralai/mistralai'; -import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// Mistral -export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - let fullText = ''; - - const thisConfig = settingsOfProvider.mistral; - - const mistral = new Mistral({ - apiKey: thisConfig.apiKey, - }) - - await mistral.chat - .stream({ - messages: messages, - model: modelName, - stream: true, - }) - .then(async response => { - // Mistral has a really nonstandard API - no interrupt and weird stream types - _setAborter(() => { console.log('Mistral does not support interrupts! Further messages will just be ignored.') }); - // when receive text - for await (const chunk of response) { - const c = chunk.data.choices[0].delta.content || '' - const newText = ( - typeof c === 'string' ? c - : c?.map(c => c.type === 'text' ? c.text : c.type).join('\n') - ) - fullText += newText; - onText({ newText, fullText }); - } - - onFinalMessage({ fullText, tools: [] }); - }) - .catch(error => { - onError({ message: error + '', fullError: error }); - }) -} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 4744db62..370d411a 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -64,6 +64,18 @@ const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }) } + else if (providerName === 'mistral') { + const thisConfig = settingsOfProvider.mistral + return new OpenAI({ + baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, + }) + } + else if (providerName === 'groq') { + const thisConfig = settingsOfProvider.groq + return new OpenAI({ + baseURL: '"https://api.groq.com/openai/v1"', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, + }) + } else { console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`) throw new Error(`providerName was invalid: ${providerName}`) @@ -167,4 +179,4 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on } }) -}; +} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 6ebdbdf6..22255292 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -10,42 +10,189 @@ import { sendAnthropicChat } from './anthropic.js'; import { sendOllamaFIM, sendOllamaChat } from './ollama.js'; import { sendOpenAIChat } from './openai.js'; import { sendGeminiChat } from './gemini.js'; -import { sendGroqChat } from './groq.js'; -import { sendMistralChat } from './mistral.js'; -import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; +import { developerInfoOfModelName, developerInfoOfProviderName, displayInfoOfProviderName, ProviderName, recognizedModelOfModelName } from '../../common/voidSettingsTypes.js'; + + +const cleanChatMessages = (modelName: string, providerName: ProviderName, messages: LLMChatMessage[]): { separateSystemMessageStr?: string, messages: _InternalLLMChatMessage[] } => { + const recognizedModel = recognizedModelOfModelName(modelName) + const { separateSystemMessage, toolsGoInRole, modelOverrides } = developerInfoOfProviderName(providerName) + + const { supportsSystemMessage, maxTokens, /* supportsTools, supportsAutocompleteFIM, supportsStreaming */ } = developerInfoOfModelName(recognizedModel, modelOverrides) -const cleanChatMessages = (messages: LLMChatMessage[]): _InternalLLMChatMessage[] => { // trim message content (Anthropic and other providers give an error if there is trailing whitespace) messages = messages.map(m => ({ ...m, content: m.content.trim() })) + + // 1. SYSTEM MESSAGE // find system messages and concatenate them - const systemMessage = messages + const systemMessageStr = messages .filter(msg => msg.role === 'system') .map(msg => msg.content) .join('\n') || undefined; - // remove all system messages - const noSystemMessages = messages - .filter(msg => msg.role !== 'system') as _InternalLLMChatMessage[] + let separateSystemMessageStr = undefined - // add system mesasges to first message (should be a user message) - if (systemMessage && (noSystemMessages.length !== 0)) { - const newFirstMessage = { - role: noSystemMessages[0].role, - content: ('' - + '\n' - + systemMessage - + '\n' - + '\n' - + noSystemMessages[0].content - ) + // remove all system messages + const noSystemMessages: _InternalLLMChatMessage[] = messages.filter(msg => msg.role !== 'system') + + if (systemMessageStr) { + // if supports system message + if (supportsSystemMessage) { + if (separateSystemMessage) + separateSystemMessageStr = systemMessageStr + else { + noSystemMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message + } + } + // if does not support system message + else { + if (supportsSystemMessage) { + if (noSystemMessages.length === 0) + noSystemMessages.push({ role: 'user', content: systemMessageStr }) + // add system mesasges to first message (should be a user message) + else { + const newFirstMessage = { + role: noSystemMessages[0].role, + content: ('' + + '\n' + + systemMessageStr + + '\n' + + '\n' + + noSystemMessages[0].content + ) + } + noSystemMessages.splice(0, 1) // delete first message + noSystemMessages.unshift(newFirstMessage) // add new first message + } + } } - noSystemMessages.splice(0, 1) // delete first message - noSystemMessages.unshift(newFirstMessage) // add new first message } - return noSystemMessages + // 2. TOOLS + + const newMessages = noSystemMessages; + + if (toolsGoInRole) { + let index = 0; + while (index < newMessages.length) { + + // merge tool with the previous assistant and the following user message + + // take prev message and add + /* +openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps +"tool_calls":[ +{ + "id": "call_12345xyz", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" + } +}] + +openai user response will be: +{ + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result) +} + +anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples +"content": [ + { + "type": "text", + "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." + }, + { + "type": "tool_use", + "id": "toolu_01A09q90qw90lq917835lq9", + "name": "get_weather", + "input": {"location": "San Francisco, CA", "unit": "celsius"} + } + ] + +anthropic user message response will be: +"content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_01A09q90qw90lq917835lq9", + "content": "15 degrees" + } +] + + +ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) +gemini request: { + "role": "assistant", + "content": null, + "function_call": { + "name": "get_weather", + "arguments": { + "latitude": 48.8566, + "longitude": 2.3522 + } + } +} +gemini response: +{ + "role": "assistant", + "function_response": { + "name": "get_weather", + "response": { + "temperature": "15°C", + "condition": "Cloudy" + } + } +} + + ++ anthropic + ++ openai-compat (4) + + gemini + +ollama + + +mistral: same as openai + + */ + + + if (newMessages[index].role === 'tool') { + const toolMessage = newMessages[index]; + const assistantMessage = newMessages[index - 1]; + const userMessage = newMessages[index + 1]; + + // while ((toolIndex = newMessages.findIndex((msg, idx) => idx > toolIndex && msg.role === 'tool')) !== -1) { + + // tool_use goes in assistant + if (assistantMessage?.role === 'assistant') { + assistantMessage.tool_use += `\n${toolMessage.content}`; + } + + // tool_result goes in user + if (userMessage?.role === 'user') { + + userMessage.content = `${toolMessage.content}\n${userMessage.content}`; + } + + // Remove the tool message after merging its content + newMessages.splice(index, 1); + } else { + index++; + } + } + } + + + + return { + separateSystemMessageStr, + messages: newMessages + } } @@ -68,11 +215,14 @@ export const sendLLMMessage = ({ ) => { let messagesArr: _InternalLLMChatMessage[] = [] + + // TODO!!! move this to the actual providers if (messagesType === 'chatMessages') { - messagesArr = cleanChatMessages([ + const { messages: cleanedMessages, separateSystemMessageStr } = cleanChatMessages(modelName, providerName, [ { role: 'system', content: aiInstructions }, ...messages_ ]) + messagesArr = cleanedMessages } // only captures number of messages and message "shape", no actual code, instructions, prompts, etc @@ -141,6 +291,8 @@ export const sendLLMMessage = ({ case 'openRouter': case 'deepseek': case 'openAICompatible': + case 'mistral': + case 'groq': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] }) else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; @@ -156,14 +308,6 @@ export const sendLLMMessage = ({ if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM', tools: [] }) else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; - case 'groq': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM', tools: [] }) - else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); - break; - case 'mistral': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM', tools: [] }) - else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); - break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) break; From 131493b5e17581dc3f043fa875b3d2824bd8fe20 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 16 Feb 2025 00:43:46 -0800 Subject: [PATCH 23/92] tool structure --- .../contrib/void/browser/chatThreadService.ts | 122 +++++----- .../contrib/void/browser/editCodeService.ts | 13 +- .../react/src/void-settings-tsx/Settings.tsx | 39 +-- .../contrib/void/common/llmMessageTypes.ts | 27 ++- .../void/common/voidSettingsService.ts | 4 +- .../contrib/void/common/voidSettingsTypes.ts | 43 ++-- .../void/electron-main/llmMessage/_old.ts | 96 ++++++++ .../electron-main/llmMessage/addSupport.ts | 177 ++++++++++++++ .../electron-main/llmMessage/anthropic.ts | 17 +- .../void/electron-main/llmMessage/gemini.ts | 43 ---- .../void/electron-main/llmMessage/ollama.ts | 137 +++++------ .../void/electron-main/llmMessage/openai.ts | 54 +++-- .../llmMessage/sendLLMMessage.ts | 224 +----------------- 13 files changed, 530 insertions(+), 466 deletions(-) create mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts create mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/addSupport.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/gemini.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 98165ce0..ec1719b8 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -15,6 +15,7 @@ import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; import { IToolsService, ToolName, voidTools } from '../common/toolsService.js'; +import { toLLMChatMessage } from '../common/llmMessageTypes.js'; // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { @@ -53,6 +54,7 @@ export type ChatMessage = } | { role: 'assistant'; + tool_calls?: { name: string, id: string, params: string }[]; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored } @@ -65,7 +67,7 @@ export type ChatMessage = role: 'tool'; name: string; // internal use params: string | null; // internal use - tool_use_id: string; // apis require this + id: string; // apis require this tool use id content: string | null; // summary of the tool to the LLM displayContent: string | null; // text message of result } @@ -296,82 +298,64 @@ class ChatThreadService extends Disposable implements IChatThreadService { // agent loop - let shouldContinue = false - do { - shouldContinue = false + const agentLoop = async () => { - let res_: () => void - const awaitable = new Promise((res, rej) => { res_ = res }) + let shouldContinue = false + do { + shouldContinue = false - const llmCancelToken = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - useProviderFor: 'Ctrl+L', - logging: { loggingName: `Agent` }, - messages: [ - { role: 'system', content: chat_systemMessage }, - ...this.getCurrentThread().messages.map(m => ({ ...m, content: m.content || '(empty model output)' })), - ], - tools: [voidTools['read_file']], // TODO!!!!! make this change on agent | chat | search + let res_: () => void + const awaitable = new Promise((res, rej) => { res_ = res }) - onText: ({ fullText }) => { - this._setStreamState(threadId, { messageSoFar: fullText }) - }, - onFinalMessage: async ({ fullText, tools }) => { - if (tools.length === 0) { - this._finishStreamingTextMessage(threadId, fullText) - } - else { - for (const tool of tools) { - if (!(tool.name in this._toolsService.toolFns)) { - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, tool_use_id: tool.tool_use_id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) - } - else { - const toolName = tool.name as ToolName - const toolResult = await this._toolsService.toolFns[toolName](tool.args) - const string = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, tool_use_id: tool.tool_use_id, content: string, displayContent: string, }) - shouldContinue = true + const llmCancelToken = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: 'Ctrl+L', + logging: { loggingName: `Agent` }, + messages: [ + { role: 'system', content: chat_systemMessage }, + ...this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))), + ], + + // TODO!!!!! make this change on 'agent' | 'chat' + tools: Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName]), + + onText: ({ fullText }) => { + this._setStreamState(threadId, { messageSoFar: fullText }) + }, + onFinalMessage: async ({ fullText, tools }) => { + if (tools.length === 0) { + this._finishStreamingTextMessage(threadId, fullText) + } + else { + for (const tool of tools) { + if (!(tool.name in this._toolsService.toolFns)) { + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, id: tool.id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) + } + else { + const toolName = tool.name as ToolName + const toolResult = await this._toolsService.toolFns[toolName](tool.args) + const string = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, id: tool.id, content: string, displayContent: string, }) + shouldContinue = true + } } } - } - res_() - }, - onError: (error) => { - this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) - res_() - }, - }) - if (llmCancelToken === null) return - this._setStreamState(threadId, { streamingToken: llmCancelToken }) + res_() + }, + onError: (error) => { + this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) + res_() + }, + }) + if (llmCancelToken === null) break + this._setStreamState(threadId, { streamingToken: llmCancelToken }) - await awaitable + await awaitable + } + while (shouldContinue); } - while (shouldContinue); - - - - // const llmCancelToken = this._llmMessageService.sendLLMMessage({ - // messagesType: 'chatMessages', - // logging: { loggingName: 'Chat' }, - // useProviderFor: 'Ctrl+L', - // messages: [ - // { role: 'system', content: chat_systemMessage }, - // ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), - // ], - // onText: ({ newText, fullText }) => { - // this._setStreamState(threadId, { messageSoFar: fullText }) - // }, - // onFinalMessage: ({ fullText: content }) => { - // this._finishStreaming(threadId, content) - // }, - // onError: (error) => { - // this._finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) - // }, - - // }) - // if (llmCancelToken === null) return - // this._setStreamState(threadId, { streamingToken: llmCancelToken }) + agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 36173d68..c2828084 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -39,7 +39,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; -import { LLMChatMessage, _InternalLLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; +import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { VSReadFile } from './helpers/readFile.js'; @@ -1178,13 +1178,16 @@ class EditCodeService extends Disposable implements IEditCodeService { private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { + console.log('SEARCHREPLACE') const uri_ = this._getActiveEditorURI() if (!uri_) return const uri = uri_ + console.log('/* AAAA */') // generate search/replace block text const fileContents = await VSReadFile(this._modelService, uri) if (fileContents === null) return + console.log('/* BBB*/') @@ -1236,9 +1239,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - - - // TODO turn this into a service and provide it + // TODO!!! turn this into a service and provide it streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', useProviderFor: 'Apply', @@ -1304,6 +1305,8 @@ class EditCodeService extends Disposable implements IEditCodeService { this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { + console.log('/* ONFIN */', fullText) + // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") const blocks = extractSearchReplaceBlocks(fullText) @@ -1322,6 +1325,8 @@ class EditCodeService extends Disposable implements IEditCodeService { onDone(false) }, onError: (e) => { + console.log('/* ERRRRRR */') + console.log('ERROR', e); onDone(true) }, diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 62511563..b1079587 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -395,7 +395,16 @@ export const AIInstructionsBox = () => { export const FeaturesTab = () => { return <> -

Local Providers

+

Models

+ + + + + + + + +

Local Providers

{/*

{`Keep your data private by hosting AI locally on your computer.`}

*/} {/*

{`Instructions:`}

*/} {/*

{`Void can access any model that you host locally. We automatically detect your local models by default.`}

*/} @@ -420,13 +429,20 @@ export const FeaturesTab = () => { -

Models

+ + +

Feature Options

- - - - + {featureNames.map(featureName => +
+

{displayInfoOfFeatureName(featureName)}

+ +
+ )}
+ } @@ -588,17 +604,6 @@ const GeneralTab = () => {
-
-

Model Selection

- {featureNames.map(featureName => -
-

{displayInfoOfFeatureName(featureName)}

- -
- )} -
} diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 27ce34c1..0af2e31e 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { ChatMessage } from '../browser/chatThreadService.js' import { InternalToolInfo } from './toolsService.js' import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' @@ -22,7 +23,7 @@ export const errorDetails = (fullError: Error | null): string | null => { } export type OnText = (p: { newText: string, fullText: string }) => void -export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string, tool_use_id: string, }[] }) => void +export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string, id: string, }[] }) => void // id is tool_use_id export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } @@ -30,20 +31,32 @@ export type LLMChatMessage = { role: 'system' | 'user'; content: string; } | { - role: 'tool'; - tool_use_id: string; + role: 'assistant', + tool_calls?: { name: string, id: string, params: string }[]; content: string; } | { - role: 'assistant', - tool_calls?: { name: string, tool_use_id: string, params: string }[]; + role: 'tool'; + id: string; content: string; } +export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { + if (c.role === 'system' || c.role === 'user') { + return { role: c.role, content: c.content ?? '(empty)' } + } + else if (c.role === 'assistant') + return { role: c.role, tool_calls: c.tool_calls, content: c.content ?? '(empty model output)' } + else if (c.role === 'tool') + return { role: c.role, id: c.id, content: c.content ?? '(empty output)' } + else { + throw 1 + } +} export type _InternalLLMChatMessage = { role: any; - tool_use_id?: any; + id?: any; content: string; } @@ -112,7 +125,7 @@ export type _InternalSendLLMChatMessageFnType = ( tools?: InternalToolInfo[], - messages: _InternalLLMChatMessage[]; + messages: LLMChatMessage[]; } ) => void diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index af68ea38..eac87692 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IMetricsService } from './metricsService.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; const STORAGE_KEY = 'void.settingsServiceStorage' @@ -89,7 +89,7 @@ const _updatedValidatedState = (state: Omit) // update model options let newModelOptions: ModelOption[] = [] for (const providerName of providerNames) { - const providerTitle = displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName + const providerTitle = providerName // displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue // if disabled, don't display model options for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) { if (isHidden) continue diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index da5aa81b..9bc1638d 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -25,9 +25,7 @@ export type DeveloperInfoAtModel = { } export type DeveloperInfoAtProvider = { - separateSystemMessage?: boolean; - toolsGoInRole?: boolean; // whether to do {role:'tool'} or {role:'user' tool:...} - modelOverrides?: Partial; // any overrides for models that a provider might have (e.g. if a provider always supports tool use, even if we don't recognize the model we can set tools to true) + overrideSettingsForAllModels?: Partial; // any overrides for models that a provider might have (e.g. if a provider always supports tool use, even if we don't recognize the model we can set tools to true) } @@ -99,37 +97,34 @@ export function recognizedModelOfModelName(modelName: string): RecognizedModelNa const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { 'anthropic': { - separateSystemMessage: true, - toolsGoInRole: false, - modelOverrides: { + overrideSettingsForAllModels: { + supportsSystemMessage: 'system', supportsTools: true, + supportsAutocompleteFIM: false, + supportsStreaming: true, } }, 'deepseek': { - separateSystemMessage: true, - }, - 'openAI': { - separateSystemMessage: false, - toolsGoInRole: true, - }, - 'gemini': { - separateSystemMessage: true, - toolsGoInRole: false - }, - 'mistral': { - separateSystemMessage: true, - }, - 'groq': { - separateSystemMessage: true, + overrideSettingsForAllModels: { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: true, + } }, 'ollama': { - separateSystemMessage: false, }, 'openRouter': { - separateSystemMessage: true, }, 'openAICompatible': { - separateSystemMessage: true, + }, + 'openAI': { + }, + 'gemini': { + }, + 'mistral': { + }, + 'groq': { }, } export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts new file mode 100644 index 00000000..e1e90245 --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts @@ -0,0 +1,96 @@ +// /*-------------------------------------------------------------------------------------- +// * Copyright 2025 Glass Devtools, Inc. All rights reserved. +// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. +// *--------------------------------------------------------------------------------------*/ + +// import Groq from 'groq-sdk'; +// import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; + +// // Groq +// export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +// let fullText = ''; + +// const thisConfig = settingsOfProvider.groq + +// const groq = new Groq({ +// apiKey: thisConfig.apiKey, +// dangerouslyAllowBrowser: true +// }); + +// await groq.chat.completions +// .create({ +// messages: messages, +// model: modelName, +// stream: true, +// }) +// .then(async response => { +// _setAborter(() => response.controller.abort()) +// // when receive text +// for await (const chunk of response) { +// const newText = chunk.choices[0]?.delta?.content || ''; +// fullText += newText; +// onText({ newText, fullText }); +// } + +// onFinalMessage({ fullText, tools: [] }); +// }) +// .catch(error => { +// onError({ message: error + '', fullError: error }); +// }) + + +// }; + + + +// /*-------------------------------------------------------------------------------------- +// * Copyright 2025 Glass Devtools, Inc. All rights reserved. +// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. +// *--------------------------------------------------------------------------------------*/ + +// import { Mistral } from '@mistralai/mistralai'; +// import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; + +// // Mistral +// export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +// let fullText = ''; + +// const thisConfig = settingsOfProvider.mistral; + +// const mistral = new Mistral({ +// apiKey: thisConfig.apiKey, +// }) + +// await mistral.chat +// .stream({ +// messages: messages, +// model: modelName, +// stream: true, +// }) +// .then(async response => { +// // Mistral has a really nonstandard API - no interrupt and weird stream types +// _setAborter(() => { console.log('Mistral does not support interrupts! Further messages will just be ignored.') }); +// // when receive text +// for await (const chunk of response) { +// const c = chunk.data.choices[0].delta.content || '' +// const newText = ( +// typeof c === 'string' ? c +// : c?.map(c => c.type === 'text' ? c.text : c.type).join('\n') +// ) +// fullText += newText; +// onText({ newText, fullText }); +// } + +// onFinalMessage({ fullText, tools: [] }); +// }) +// .catch(error => { +// onError({ message: error + '', fullError: error }); +// }) +// } + + + + + + + diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/addSupport.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/addSupport.ts new file mode 100644 index 00000000..ef9dfdd5 --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/addSupport.ts @@ -0,0 +1,177 @@ +import { _InternalLLMChatMessage, LLMChatMessage } from '../../common/llmMessageTypes.js'; +import { DeveloperInfoAtModel, developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js'; + + +// no matter whether the model supports a system message or not (or what format it supports), add it in some way +// also take into account tools if the model doesn't support tool use +export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: _InternalLLMChatMessage[], devInfo: DeveloperInfoAtModel } => { + + const messages: _InternalLLMChatMessage[] = messages_.map(m => ({ ...m, content: m.content.trim(), })) + + const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) + const devInfo = developerInfoOfModelName(modelName, overrideSettingsForAllModels) + const { supportsSystemMessage } = devInfo + + // 1. SYSTEM MESSAGE + // find system messages and concatenate them + let systemMessageStr = messages + .filter(msg => msg.role === 'system') + .map(msg => msg.content) + .join('\n') || undefined; + + let separateSystemMessageStr: string | undefined = undefined + + // remove all system messages + const newMessages: _InternalLLMChatMessage[] = messages.filter(msg => msg.role !== 'system') + + + // if (!supportsTools) { + // if (!systemMessageStr) systemMessageStr = '' + // systemMessageStr += '' // TODO!!! add tool use system message here + // } + + + if (systemMessageStr) { + // if supports system message + if (supportsSystemMessage) { + if (separateSystemMessage) + separateSystemMessageStr = systemMessageStr + else { + newMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message + } + } + // if does not support system message + else { + if (supportsSystemMessage) { + if (newMessages.length === 0) + newMessages.push({ role: 'user', content: systemMessageStr }) + // add system mesasges to first message (should be a user message) + else { + const newFirstMessage = { + role: newMessages[0].role, + content: ('' + + '\n' + + systemMessageStr + + '\n' + + '\n' + + newMessages[0].content + ) + } + newMessages.splice(0, 1) // delete first message + newMessages.unshift(newFirstMessage) // add new first message + } + } + } + } + + + return { + separateSystemMessageStr, + messages: newMessages, + devInfo, + } +} + + + + + +// const { maxTokens, supportsTools, supportsAutocompleteFIM, supportsStreaming, } = developerInfoOfModelName(recognizedModel) + + + + + + +// let index = 0; +// while (index < newMessages.length) { + +// merge tool with the previous assistant and the following user message + +// take prev message and add +/* +openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps +"tool_calls":[ +{ +"id": "call_12345xyz", +"type": "function", +"function": { +"name": "get_weather", +"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" +} +}] + +openai user response will be: +{ +"role": "tool", +"tool_call_id": tool_call.id, +"content": str(result) +} + +anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples +"content": [ +{ +"type": "text", +"text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." +}, +{ +"type": "tool_use", +"id": "toolu_01A09q90qw90lq917835lq9", +"name": "get_weather", +"input": {"location": "San Francisco, CA", "unit": "celsius"} +} +] + +anthropic user message response will be: +"content": [ +{ +"type": "tool_result", +"tool_use_id": "toolu_01A09q90qw90lq917835lq9", +"content": "15 degrees" +} +] + + +*/ + + + +/* + + +ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) +gemini request: { +"role": "assistant", +"content": null, +"function_call": { +"name": "get_weather", +"arguments": { +"latitude": 48.8566, +"longitude": 2.3522 +} +} +} +gemini response: +{ +"role": "assistant", +"function_response": { +"name": "get_weather", +"response": { +"temperature": "15°C", +"condition": "Cloudy" +} +} +} + + ++ anthropic + ++ openai-compat (4) ++ gemini + +ollama + + +mistral: same as openai + +*/ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index b443cca1..77bbcb82 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -7,6 +7,7 @@ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; import { InternalToolInfo } from '../../common/toolsService.js'; +import { addSystemMessageAndToolSupport } from './addSupport.js'; @@ -28,7 +29,7 @@ export const toAnthropicTool = (toolInfo: InternalToolInfo) => { -export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, tools }) => { +export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, tools: tools_ }) => { const thisConfig = settingsOfProvider.anthropic @@ -38,15 +39,19 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, return } + const { messages, separateSystemMessageStr, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, { separateSystemMessage: true }) + const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); + const tools = devInfo?.supportsTools && (tools_?.length ?? 0) !== 0 ? tools_?.map(tool => toAnthropicTool(tool)) : undefined + const stream = anthropic.messages.stream({ - // system: systemMessage, + system: separateSystemMessageStr, messages: messages, model: modelName, max_tokens: maxTokens, - tools: tools?.map(tool => toAnthropicTool(tool)), - tool_choice: { type: 'auto', disable_parallel_tool_use: true } // one tool use at a time + tools: tools, + tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time }) @@ -78,9 +83,9 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, stream.on('finalMessage', (response) => { // stringify the response's content const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input), tool_use_id: c.id } : null).filter(c => !!c) + // const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input), tool_use_id: c.id } : null).filter(c => !!c) - onFinalMessage({ fullText: content, tools }) + onFinalMessage({ fullText: content, tools: [] }) }) stream.on('error', (error) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/gemini.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/gemini.ts deleted file mode 100644 index 2732fbe4..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/gemini.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Content, GoogleGenerativeAI } from '@google/generative-ai'; -import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// Gemini -export const sendGeminiChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - - let fullText = '' - - const thisConfig = settingsOfProvider.gemini - - const genAI = new GoogleGenerativeAI(thisConfig.apiKey); - const model = genAI.getGenerativeModel({ model: modelName }); - - // Convert messages to Gemini format - const geminiMessages: Content[] = messages - .map((msg, i) => ({ - parts: [{ text: msg.content }], - role: msg.role === 'assistant' ? 'model' : 'user' - })) - - model.generateContentStream({ - // systemInstruction: systemMessage, - contents: geminiMessages, - }) - .then(async response => { - _setAborter(() => response.stream.return(fullText)) - - for await (const chunk of response.stream) { - const newText = chunk.text(); - fullText += newText; - onText({ newText, fullText }); - } - onFinalMessage({ fullText, tools: [] }); - }) - .catch((error) => { - onError({ message: error + '', fullError: error }) - }) -} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts index daff3a29..da6715c0 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts @@ -1,3 +1,4 @@ + /*-------------------------------------------------------------------------------------- * Copyright 2025 Glass Devtools, Inc. All rights reserved. * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. @@ -38,84 +39,86 @@ export const ollamaList: _InternalModelListFnType = async ( } -export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - - const thisConfig = settingsOfProvider.ollama - // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in - if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - - let fullText = '' - - const ollama = new Ollama({ host: thisConfig.endpoint }) - - ollama.generate({ - model: modelName, - prompt: messages.prefix, - suffix: messages.suffix, - options: { - stop: messages.stopTokens, - num_predict: 300, // max tokens - // repeat_penalty: 1, - }, - raw: true, - stream: true, - }) - .then(async stream => { - _setAborter(() => stream.abort()) - // iterate through the stream - for await (const chunk of stream) { - const newText = chunk.response; - fullText += newText; - onText({ newText, fullText }); - } - onFinalMessage({ fullText, tools: [] }); - }) - // when error/fail - .catch((error) => { - onError({ message: error + '', fullError: error }) - }) -}; -// Ollama -export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +// export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - const thisConfig = settingsOfProvider.ollama - // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in - if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) +// const thisConfig = settingsOfProvider.ollama +// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in +// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - let fullText = '' +// let fullText = '' - const ollama = new Ollama({ host: thisConfig.endpoint }) +// const ollama = new Ollama({ host: thisConfig.endpoint }) - ollama.chat({ - model: modelName, - messages: messages, - stream: true, - // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens - }) - .then(async stream => { - _setAborter(() => stream.abort()) - // iterate through the stream - for await (const chunk of stream) { - const newText = chunk.message.content; +// ollama.generate({ +// model: modelName, +// prompt: messages.prefix, +// suffix: messages.suffix, +// options: { +// stop: messages.stopTokens, +// num_predict: 300, // max tokens +// // repeat_penalty: 1, +// }, +// raw: true, +// stream: true, +// }) +// .then(async stream => { +// _setAborter(() => stream.abort()) +// // iterate through the stream +// for await (const chunk of stream) { +// const newText = chunk.response; +// fullText += newText; +// onText({ newText, fullText }); +// } +// onFinalMessage({ fullText, tools: [] }); +// }) +// // when error/fail +// .catch((error) => { +// onError({ message: error + '', fullError: error }) +// }) +// }; - // chunk.message.tool_calls[0].function.arguments - fullText += newText; - onText({ newText, fullText }); - } +// // Ollama +// export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - onFinalMessage({ fullText, tools: [] }); +// const thisConfig = settingsOfProvider.ollama +// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in +// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - }) - // when error/fail - .catch((error) => { - onError({ message: error + '', fullError: error }) - }) +// let fullText = '' -}; +// const ollama = new Ollama({ host: thisConfig.endpoint }) + +// ollama.chat({ +// model: modelName, +// messages: messages, +// stream: true, +// // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens +// }) +// .then(async stream => { +// _setAborter(() => stream.abort()) +// // iterate through the stream +// for await (const chunk of stream) { +// const newText = chunk.message.content; + +// // chunk.message.tool_calls[0].function.arguments + +// fullText += newText; +// onText({ newText, fullText }); +// } + +// onFinalMessage({ fullText, tools: [] }); + +// }) +// // when error/fail +// .catch((error) => { +// onError({ message: error + '', fullError: error }) +// }) + +// }; -// ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',] +// // ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',] diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 370d411a..b7c81563 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -7,6 +7,7 @@ import OpenAI from 'openai'; import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { Model } from 'openai/resources/models.js'; import { InternalToolInfo } from '../../common/toolsService.js'; +import { addSystemMessageAndToolSupport } from './addSupport.js'; // import { parseMaxTokensStr } from './util.js'; @@ -38,11 +39,19 @@ type NewParams = Pick[0] & Paramet const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { if (providerName === 'openAI') { - const thisConfig = settingsOfProvider.openAI - return new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ + apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true + }) + } + else if (providerName === 'ollama') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ + baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, + }) } else if (providerName === 'openRouter') { - const thisConfig = settingsOfProvider.openRouter + const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, defaultHeaders: { @@ -51,33 +60,38 @@ const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { }, }) } + else if (providerName === 'gemini') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ + baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, + }) + } else if (providerName === 'deepseek') { - const thisConfig = settingsOfProvider.deepseek + const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, }) - } else if (providerName === 'openAICompatible') { - const thisConfig = settingsOfProvider.openAICompatible + const thisConfig = settingsOfProvider[providerName] return new OpenAI({ - baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true + baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, }) } else if (providerName === 'mistral') { - const thisConfig = settingsOfProvider.mistral + const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, }) } else if (providerName === 'groq') { - const thisConfig = settingsOfProvider.groq + const thisConfig = settingsOfProvider[providerName] return new OpenAI({ - baseURL: '"https://api.groq.com/openai/v1"', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, + baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, }) } else { - console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`) + console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`) throw new Error(`providerName was invalid: ${providerName}`) } } @@ -130,10 +144,14 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe // OpenAI, OpenRouter, OpenAICompatible -export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }) => { +export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools: tools_ }) => { let fullText = '' - const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} + const toolCallOfIndex: { [index: string]: { name: string, args: string, id: string } } = {} + + const { messages, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, { separateSystemMessage: false }) + + const tools = devInfo?.supportsTools && (tools_?.length ?? 0) !== 0 ? tools_?.map(tool => toOpenAITool(tool)) : undefined const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider }) @@ -141,7 +159,9 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on model: modelName, messages: messages, stream: true, - tools: tools?.map(tool => toOpenAITool(tool)), + tools: tools, + tool_choice: tools ? 'auto' : undefined, + parallel_tool_calls: tools ? false : undefined, } openai.chat.completions @@ -155,9 +175,11 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on // tool call for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) { const index = tool.index - if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' } + if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '', id: '' } toolCallOfIndex[index].name += tool.function?.name ?? '' - toolCallOfIndex[index].args += tool.function?.arguments ?? '' + toolCallOfIndex[index].args += tool.function?.arguments ?? ''; + toolCallOfIndex[index].id = tool.id ?? '' + } // message diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 22255292..dec84841 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -3,197 +3,12 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, LLMChatMessage, _InternalLLMChatMessage } from '../../common/llmMessageTypes.js'; +import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, _InternalLLMChatMessage } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; +import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; import { sendAnthropicChat } from './anthropic.js'; -import { sendOllamaFIM, sendOllamaChat } from './ollama.js'; import { sendOpenAIChat } from './openai.js'; -import { sendGeminiChat } from './gemini.js'; -import { developerInfoOfModelName, developerInfoOfProviderName, displayInfoOfProviderName, ProviderName, recognizedModelOfModelName } from '../../common/voidSettingsTypes.js'; - - -const cleanChatMessages = (modelName: string, providerName: ProviderName, messages: LLMChatMessage[]): { separateSystemMessageStr?: string, messages: _InternalLLMChatMessage[] } => { - const recognizedModel = recognizedModelOfModelName(modelName) - const { separateSystemMessage, toolsGoInRole, modelOverrides } = developerInfoOfProviderName(providerName) - - const { supportsSystemMessage, maxTokens, /* supportsTools, supportsAutocompleteFIM, supportsStreaming */ } = developerInfoOfModelName(recognizedModel, modelOverrides) - - - // trim message content (Anthropic and other providers give an error if there is trailing whitespace) - messages = messages.map(m => ({ ...m, content: m.content.trim() })) - - - // 1. SYSTEM MESSAGE - // find system messages and concatenate them - const systemMessageStr = messages - .filter(msg => msg.role === 'system') - .map(msg => msg.content) - .join('\n') || undefined; - - let separateSystemMessageStr = undefined - - // remove all system messages - const noSystemMessages: _InternalLLMChatMessage[] = messages.filter(msg => msg.role !== 'system') - - if (systemMessageStr) { - // if supports system message - if (supportsSystemMessage) { - if (separateSystemMessage) - separateSystemMessageStr = systemMessageStr - else { - noSystemMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message - } - } - // if does not support system message - else { - if (supportsSystemMessage) { - if (noSystemMessages.length === 0) - noSystemMessages.push({ role: 'user', content: systemMessageStr }) - // add system mesasges to first message (should be a user message) - else { - const newFirstMessage = { - role: noSystemMessages[0].role, - content: ('' - + '\n' - + systemMessageStr - + '\n' - + '\n' - + noSystemMessages[0].content - ) - } - noSystemMessages.splice(0, 1) // delete first message - noSystemMessages.unshift(newFirstMessage) // add new first message - } - } - } - } - - // 2. TOOLS - - const newMessages = noSystemMessages; - - if (toolsGoInRole) { - let index = 0; - while (index < newMessages.length) { - - // merge tool with the previous assistant and the following user message - - // take prev message and add - /* -openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps -"tool_calls":[ -{ - "id": "call_12345xyz", - "type": "function", - "function": { - "name": "get_weather", - "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" - } -}] - -openai user response will be: -{ - "role": "tool", - "tool_call_id": tool_call.id, - "content": str(result) -} - -anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples -"content": [ - { - "type": "text", - "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." - }, - { - "type": "tool_use", - "id": "toolu_01A09q90qw90lq917835lq9", - "name": "get_weather", - "input": {"location": "San Francisco, CA", "unit": "celsius"} - } - ] - -anthropic user message response will be: -"content": [ - { - "type": "tool_result", - "tool_use_id": "toolu_01A09q90qw90lq917835lq9", - "content": "15 degrees" - } -] - - -ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) -gemini request: { - "role": "assistant", - "content": null, - "function_call": { - "name": "get_weather", - "arguments": { - "latitude": 48.8566, - "longitude": 2.3522 - } - } -} -gemini response: -{ - "role": "assistant", - "function_response": { - "name": "get_weather", - "response": { - "temperature": "15°C", - "condition": "Cloudy" - } - } -} - - -+ anthropic - -+ openai-compat (4) - + gemini - -ollama - - -mistral: same as openai - - */ - - - if (newMessages[index].role === 'tool') { - const toolMessage = newMessages[index]; - const assistantMessage = newMessages[index - 1]; - const userMessage = newMessages[index + 1]; - - // while ((toolIndex = newMessages.findIndex((msg, idx) => idx > toolIndex && msg.role === 'tool')) !== -1) { - - // tool_use goes in assistant - if (assistantMessage?.role === 'assistant') { - assistantMessage.tool_use += `\n${toolMessage.content}`; - } - - // tool_result goes in user - if (userMessage?.role === 'user') { - - userMessage.content = `${toolMessage.content}\n${userMessage.content}`; - } - - // Remove the tool message after merging its content - newMessages.splice(index, 1); - } else { - index++; - } - } - } - - - - return { - separateSystemMessageStr, - messages: newMessages - } -} export const sendLLMMessage = ({ @@ -214,16 +29,6 @@ export const sendLLMMessage = ({ metricsService: IMetricsService ) => { - let messagesArr: _InternalLLMChatMessage[] = [] - - // TODO!!! move this to the actual providers - if (messagesType === 'chatMessages') { - const { messages: cleanedMessages, separateSystemMessageStr } = cleanChatMessages(modelName, providerName, [ - { role: 'system', content: aiInstructions }, - ...messages_ - ]) - messagesArr = cleanedMessages - } // only captures number of messages and message "shape", no actual code, instructions, prompts, etc const captureLLMEvent = (eventId: string, extras?: object) => { @@ -231,8 +36,8 @@ export const sendLLMMessage = ({ providerName, modelName, ...messagesType === 'chatMessages' ? { - numMessages: messagesArr?.length, - messagesShape: messagesArr?.map(msg => ({ role: msg.role, length: msg.content.length })), + numMessages: messages_?.length, + messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), origNumMessages: messages_?.length, origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), @@ -283,7 +88,10 @@ export const sendLLMMessage = ({ } abortRef_.current = onAbort - captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messagesArr[messagesArr.length - 1]?.content.length }) + if (messagesType === 'chatMessages') + captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messages_[messages_.length - 1]?.content.length }) + else if (messagesType === 'FIMMessage') + captureLLMEvent(`${loggingName} - Sending FIM`, {}) // TODO!!! add more metrics try { switch (providerName) { @@ -292,21 +100,15 @@ export const sendLLMMessage = ({ case 'deepseek': case 'openAICompatible': case 'mistral': - case 'groq': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] }) - else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); - break; case 'ollama': - if (messagesType === 'FIMMessage') sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); - else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); + case 'groq': + case 'gemini': + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] }) + else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; case 'anthropic': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', tools: [] }) - else /* */ sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); - break; - case 'gemini': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM', tools: [] }) - else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); + else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) From 7244d433dda9cf874d2e2e636782e9971997b455 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 16 Feb 2025 02:53:56 -0800 Subject: [PATCH 24/92] tools should work! --- .../contrib/void/browser/chatThreadService.ts | 15 +- .../contrib/void/browser/editCodeService.ts | 5 +- .../react/src/void-settings-tsx/Settings.tsx | 2 +- .../contrib/void/common/llmMessageTypes.ts | 17 +- .../electron-main/llmMessage/addSupport.ts | 177 ----------- .../electron-main/llmMessage/anthropic.ts | 6 +- .../void/electron-main/llmMessage/openai.ts | 6 +- .../llmMessage/processMessages.ts | 294 ++++++++++++++++++ .../llmMessage/sendLLMMessage.ts | 6 +- 9 files changed, 322 insertions(+), 206 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/addSupport.ts create mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index ec1719b8..d3dedf84 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -14,7 +14,7 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; -import { IToolsService, ToolName, voidTools } from '../common/toolsService.js'; +import { InternalToolInfo, IToolsService, ToolName, voidTools } from '../common/toolsService.js'; import { toLLMChatMessage } from '../common/llmMessageTypes.js'; // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) @@ -66,10 +66,10 @@ export type ChatMessage = | { role: 'tool'; name: string; // internal use - params: string | null; // internal use + params: string; // internal use id: string; // apis require this tool use id - content: string | null; // summary of the tool to the LLM - displayContent: string | null; // text message of result + content: string; // result + displayContent: string; // text message of result } // a 'thread' means a chat message history @@ -296,6 +296,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { error: undefined }) + const tools: InternalToolInfo[] | undefined = ( + chatMode === 'chat' ? undefined + : chatMode === 'agent' ? Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName]) + : undefined) // agent loop const agentLoop = async () => { @@ -316,8 +320,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { ...this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))), ], - // TODO!!!!! make this change on 'agent' | 'chat' - tools: Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName]), + tools: tools, onText: ({ fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index c2828084..0138cfc1 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1261,6 +1261,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!(blockNum in diffareaidOfBlockNum)) { const foundInCode = findTextInCode(block.orig, fileContents) if (typeof foundInCode === 'string') { + // TODO!!! log and retry console.log('NOT FOUND IN CODE!!!!', foundInCode) continue } @@ -1305,7 +1306,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { - console.log('/* ONFIN */', fullText) + console.log('/* ON FINALMESSAGE */', fullText) // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") @@ -1325,8 +1326,6 @@ class EditCodeService extends Disposable implements IEditCodeService { onDone(false) }, onError: (e) => { - console.log('/* ERRRRRR */') - console.log('ERROR', e); onDone(true) }, diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index b1079587..cbf8607c 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -385,7 +385,7 @@ export const AIInstructionsBox = () => { return { voidSettingsService.setGlobalSetting('aiInstructions', newText) diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 0af2e31e..68aafa64 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -32,12 +32,13 @@ export type LLMChatMessage = { content: string; } | { role: 'assistant', - tool_calls?: { name: string, id: string, params: string }[]; content: string; } | { role: 'tool'; + content: string; // result + name: string; + params: string; id: string; - content: string; } export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { @@ -45,21 +46,15 @@ export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { return { role: c.role, content: c.content ?? '(empty)' } } else if (c.role === 'assistant') - return { role: c.role, tool_calls: c.tool_calls, content: c.content ?? '(empty model output)' } + return { role: c.role, content: c.content ?? '(empty model output)' } else if (c.role === 'tool') - return { role: c.role, id: c.id, content: c.content ?? '(empty output)' } + return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content ?? '(empty output)' } else { throw 1 } } -export type _InternalLLMChatMessage = { - role: any; - id?: any; - content: string; -} - type _InternalSendFIMMessage = { prefix: string; suffix: string; @@ -115,6 +110,8 @@ export type EventLLMMessageOnErrorParams = Parameters[0] & { requestId: export type _InternalSendLLMChatMessageFnType = ( params: { + aiInstructions: string; + onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/addSupport.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/addSupport.ts deleted file mode 100644 index ef9dfdd5..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/addSupport.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { _InternalLLMChatMessage, LLMChatMessage } from '../../common/llmMessageTypes.js'; -import { DeveloperInfoAtModel, developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js'; - - -// no matter whether the model supports a system message or not (or what format it supports), add it in some way -// also take into account tools if the model doesn't support tool use -export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: _InternalLLMChatMessage[], devInfo: DeveloperInfoAtModel } => { - - const messages: _InternalLLMChatMessage[] = messages_.map(m => ({ ...m, content: m.content.trim(), })) - - const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const devInfo = developerInfoOfModelName(modelName, overrideSettingsForAllModels) - const { supportsSystemMessage } = devInfo - - // 1. SYSTEM MESSAGE - // find system messages and concatenate them - let systemMessageStr = messages - .filter(msg => msg.role === 'system') - .map(msg => msg.content) - .join('\n') || undefined; - - let separateSystemMessageStr: string | undefined = undefined - - // remove all system messages - const newMessages: _InternalLLMChatMessage[] = messages.filter(msg => msg.role !== 'system') - - - // if (!supportsTools) { - // if (!systemMessageStr) systemMessageStr = '' - // systemMessageStr += '' // TODO!!! add tool use system message here - // } - - - if (systemMessageStr) { - // if supports system message - if (supportsSystemMessage) { - if (separateSystemMessage) - separateSystemMessageStr = systemMessageStr - else { - newMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message - } - } - // if does not support system message - else { - if (supportsSystemMessage) { - if (newMessages.length === 0) - newMessages.push({ role: 'user', content: systemMessageStr }) - // add system mesasges to first message (should be a user message) - else { - const newFirstMessage = { - role: newMessages[0].role, - content: ('' - + '\n' - + systemMessageStr - + '\n' - + '\n' - + newMessages[0].content - ) - } - newMessages.splice(0, 1) // delete first message - newMessages.unshift(newFirstMessage) // add new first message - } - } - } - } - - - return { - separateSystemMessageStr, - messages: newMessages, - devInfo, - } -} - - - - - -// const { maxTokens, supportsTools, supportsAutocompleteFIM, supportsStreaming, } = developerInfoOfModelName(recognizedModel) - - - - - - -// let index = 0; -// while (index < newMessages.length) { - -// merge tool with the previous assistant and the following user message - -// take prev message and add -/* -openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps -"tool_calls":[ -{ -"id": "call_12345xyz", -"type": "function", -"function": { -"name": "get_weather", -"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" -} -}] - -openai user response will be: -{ -"role": "tool", -"tool_call_id": tool_call.id, -"content": str(result) -} - -anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples -"content": [ -{ -"type": "text", -"text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." -}, -{ -"type": "tool_use", -"id": "toolu_01A09q90qw90lq917835lq9", -"name": "get_weather", -"input": {"location": "San Francisco, CA", "unit": "celsius"} -} -] - -anthropic user message response will be: -"content": [ -{ -"type": "tool_result", -"tool_use_id": "toolu_01A09q90qw90lq917835lq9", -"content": "15 degrees" -} -] - - -*/ - - - -/* - - -ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) -gemini request: { -"role": "assistant", -"content": null, -"function_call": { -"name": "get_weather", -"arguments": { -"latitude": 48.8566, -"longitude": 2.3522 -} -} -} -gemini response: -{ -"role": "assistant", -"function_response": { -"name": "get_weather", -"response": { -"temperature": "15°C", -"condition": "Cloudy" -} -} -} - - -+ anthropic - -+ openai-compat (4) -+ gemini - -ollama - - -mistral: same as openai - -*/ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index 77bbcb82..b047a601 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -7,7 +7,7 @@ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; import { InternalToolInfo } from '../../common/toolsService.js'; -import { addSystemMessageAndToolSupport } from './addSupport.js'; +import { addSystemMessageAndToolSupport } from './processMessages.js'; @@ -29,7 +29,7 @@ export const toAnthropicTool = (toolInfo: InternalToolInfo) => { -export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, tools: tools_ }) => { +export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => { const thisConfig = settingsOfProvider.anthropic @@ -39,7 +39,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: return } - const { messages, separateSystemMessageStr, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, { separateSystemMessage: true }) + const { messages, separateSystemMessageStr, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: true }) const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index b7c81563..60a66881 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -7,7 +7,7 @@ import OpenAI from 'openai'; import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { Model } from 'openai/resources/models.js'; import { InternalToolInfo } from '../../common/toolsService.js'; -import { addSystemMessageAndToolSupport } from './addSupport.js'; +import { addSystemMessageAndToolSupport } from './processMessages.js'; // import { parseMaxTokensStr } from './util.js'; @@ -144,12 +144,12 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe // OpenAI, OpenRouter, OpenAICompatible -export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools: tools_ }) => { +export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { let fullText = '' const toolCallOfIndex: { [index: string]: { name: string, args: string, id: string } } = {} - const { messages, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, { separateSystemMessage: false }) + const { messages, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: false }) const tools = devInfo?.supportsTools && (tools_?.length ?? 0) !== 0 ? tools_?.map(tool => toOpenAITool(tool)) : undefined diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts new file mode 100644 index 00000000..f6f6cd5e --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts @@ -0,0 +1,294 @@ + + +import { LLMChatMessage } from '../../common/llmMessageTypes.js'; +import { DeveloperInfoAtModel, developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js'; +import { deepClone } from '../../../../../base/common/objects.js'; + + + + +// no matter whether the model supports a system message or not (or what format it supports), add it in some way +// also take into account tools if the model doesn't support tool use +export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], aiInstructions: string, { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: any[], devInfo: DeveloperInfoAtModel } => { + + const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), })) + + const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) + const devInfo = developerInfoOfModelName(modelName, overrideSettingsForAllModels) + const { supportsSystemMessage, supportsTools } = devInfo + + // 1. SYSTEM MESSAGE + // find system messages and concatenate them + let systemMessageStr = messages + .filter(msg => msg.role === 'system') + .map(msg => msg.content) + .join('\n') || undefined; + + if (aiInstructions) + systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}` + + + let separateSystemMessageStr: string | undefined = undefined + + // remove all system messages + const newMessages: (LLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system') + + + // if (!supportsTools) { + // if (!systemMessageStr) systemMessageStr = '' + // systemMessageStr += '' // TODO!!! add tool use system message here + // } + + + if (systemMessageStr) { + // if supports system message + if (supportsSystemMessage) { + if (separateSystemMessage) + separateSystemMessageStr = systemMessageStr + else { + newMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message + } + } + // if does not support system message + else { + if (supportsSystemMessage) { + if (newMessages.length === 0) + newMessages.push({ role: 'user', content: systemMessageStr }) + // add system mesasges to first message (should be a user message) + else { + const newFirstMessage = { + role: 'user', + content: ('' + + '\n' + + systemMessageStr + + '\n' + + '\n' + + newMessages[0].content + ) + } as const + newMessages.splice(0, 1) // delete first message + newMessages.unshift(newFirstMessage) // add new first message + } + } + } + } + + + + + + + // 2. MAKE TOOLS FORMAT CORRECT in messages + let finalMessages: any[] + if (!supportsTools) { + // do nothing + finalMessages = newMessages + } + + // anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples + // "content": [ + // { + // "type": "text", + // "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." + // }, + // { + // "type": "tool_use", + // "id": "toolu_01A09q90qw90lq917835lq9", + // "name": "get_weather", + // "input": { "location": "San Francisco, CA", "unit": "celsius" } + // } + // ] + + // anthropic user message response will be: + // "content": [ + // { + // "type": "tool_result", + // "tool_use_id": "toolu_01A09q90qw90lq917835lq9", + // "content": "15 degrees" + // } + // ] + + + else if (providerName === 'anthropic') { // convert role:'tool' to anthropic's type + const newMessagesTools: ( + Exclude | { + role: 'assistant', + content: string | ({ + type: 'text'; + text: string; + } | { + type: 'tool_use'; + name: string; + input: string; + id: string; + })[] + } | { + role: 'user', + content: string | ({ + type: 'text'; + text: string; + } | { + type: 'tool_response'; + tool_use_id: string; + content: string; + })[] + } + )[] = newMessages; + + + for (let i = 0; i < newMessagesTools.length; i += 1) { + const currMsg = newMessagesTools[i] + + if (currMsg.role !== 'tool') continue + + const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined + const nextMsg = 0 <= i + 1 && i + 1 <= newMessagesTools.length ? newMessagesTools[i + 1] : undefined + + if (prevMsg?.role === 'assistant') { + if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: typeof prevMsg.content }] + prevMsg.content.push({ type: 'tool_use', name: currMsg.name, input: currMsg.params, id: currMsg.id }) + } + if (nextMsg?.role === 'user') { + if (typeof nextMsg.content === 'string') nextMsg.content = [{ type: 'text', text: typeof nextMsg.content }] + nextMsg.content.push({ type: 'tool_response', tool_use_id: currMsg.id, content: currMsg.content }) + } + } + finalMessages = newMessagesTools + } + + // openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps + // "tool_calls":[ + // { + // "type": "function", + // "id": "call_12345xyz", + // "function": { + // "name": "get_weather", + // "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" + // } + // }] + + // openai user response will be: + // { + // "role": "tool", + // "tool_call_id": tool_call.id, + // "content": str(result) + // } + + // treat all other providers like openai tool message for now + else { + + const newMessagesTools: ( + Exclude | { + role: 'assistant', + content: string; + tool_calls?: { + type: 'function'; + id: string; + function: { + name: string; + arguments: string; + } + }[] + } | { + role: 'tool', + id: string; // old val + tool_call_id: string; // new val + content: string; + } + )[] = []; + + for (let i = 0; i < newMessages.length; i += 1) { + const currMsg = newMessages[i] + + if (currMsg.role !== 'tool') { + newMessagesTools.push(currMsg) + continue + } + + // edit previous assistant message to have called the tool + const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined + if (prevMsg?.role === 'assistant') { + prevMsg.tool_calls = [{ + type: 'function', + id: currMsg.id, + function: { + name: currMsg.name, + arguments: currMsg.params + } + }] + } + + // add the tool + newMessagesTools.push({ + role: 'tool', + id: currMsg.id, + content: currMsg.content, + tool_call_id: currMsg.id, + }) + } + finalMessages = newMessagesTools + } + + + + + // 3. CROP MESSAGES SO EVERYTHING FITS IN CONTEXT + // TODO!!! + + + + + return { + separateSystemMessageStr, + messages: finalMessages, + devInfo, + } +} + + + + + + + + + +/* + + +ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) +gemini request: { +"role": "assistant", +"content": null, +"function_call": { +"name": "get_weather", +"arguments": { +"latitude": 48.8566, +"longitude": 2.3522 +} +} +} +gemini response: +{ +"role": "assistant", +"function_response": { +"name": "get_weather", +"response": { +"temperature": "15°C", +"condition": "Cloudy" +} +} +} + + ++ anthropic + ++ openai-compat (4) ++ gemini + +ollama + + +mistral: same as openai + +*/ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index dec84841..e568d3b5 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, _InternalLLMChatMessage } from '../../common/llmMessageTypes.js'; +import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; @@ -104,11 +104,11 @@ export const sendLLMMessage = ({ case 'groq': case 'gemini': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] }) - else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); + else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); break; case 'anthropic': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', tools: [] }) - else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); + else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) From af41d6a43932d9c91cdceb04acb4596e6cda5a38 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 16 Feb 2025 03:23:37 -0800 Subject: [PATCH 25/92] tool improvements --- .../contrib/void/browser/chatThreadService.ts | 12 ++++---- .../contrib/void/common/llmMessageTypes.ts | 2 +- .../contrib/void/common/voidSettingsTypes.ts | 28 +++++++++---------- .../electron-main/llmMessage/anthropic.ts | 9 ++++-- .../void/electron-main/llmMessage/openai.ts | 13 +++++---- .../llmMessage/processMessages.ts | 10 +++---- 6 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index d3dedf84..4565b6a6 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -326,19 +326,21 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { messageSoFar: fullText }) }, onFinalMessage: async ({ fullText, tools }) => { - if (tools.length === 0) { + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText, tool_calls: tools }) + + if ((tools?.length ?? 0) === 0) { this._finishStreamingTextMessage(threadId, fullText) } else { - for (const tool of tools) { + for (const tool of tools ?? []) { if (!(tool.name in this._toolsService.toolFns)) { - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, id: tool.id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.params, id: tool.id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) } else { const toolName = tool.name as ToolName - const toolResult = await this._toolsService.toolFns[toolName](tool.args) + const toolResult = await this._toolsService.toolFns[toolName](tool.params) const string = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, id: tool.id, content: string, displayContent: string, }) + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.params, id: tool.id, content: string, displayContent: string, }) shouldContinue = true } } diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 68aafa64..e82da2cb 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -23,7 +23,7 @@ export const errorDetails = (fullError: Error | null): string | null => { } export type OnText = (p: { newText: string, fullText: string }) => void -export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string, id: string, }[] }) => void // id is tool_use_id +export type OnFinalMessage = (p: { fullText: string, tools?: { name: string, params: string, id: string, }[] }) => void // id is tool_use_id export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 9bc1638d..0bbcfcde 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -18,7 +18,7 @@ export type DeveloperInfoAtModel = { // UNUSED (coming soon): recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized supportsTools: boolean, // we will just do a string of tool use if it doesn't support - supportsSystemMessage: 'developer' | 'system' | false, // if null, we will just do a string of system message + supportsSystemMessageRole: 'developer' | 'system' | false, // if null, we will just do a string of system message. this is independent from separateSystemMessage, which takes priority and is passed directly in each provider's implementation. supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> supportsStreaming: boolean, // (o1 does NOT) we will just dump the final result if doesn't support it maxTokens: number, // required @@ -98,7 +98,7 @@ export function recognizedModelOfModelName(modelName: string): RecognizedModelNa const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { 'anthropic': { overrideSettingsForAllModels: { - supportsSystemMessage: 'system', + supportsSystemMessageRole: 'system', supportsTools: true, supportsAutocompleteFIM: false, supportsStreaming: true, @@ -106,7 +106,7 @@ const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAt }, 'deepseek': { overrideSettingsForAllModels: { - supportsSystemMessage: false, + supportsSystemMessageRole: false, supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: true, @@ -137,15 +137,15 @@ export const developerInfoOfProviderName = (providerName: ProviderName): Partial // providerName is optional, but gives some extra fallbacks if provided const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { 'OpenAI 4o': { - supportsSystemMessage: false, - supportsTools: false, + supportsSystemMessageRole: 'system', + supportsTools: true, supportsAutocompleteFIM: false, - supportsStreaming: false, + supportsStreaming: true, maxTokens: 4096, }, 'Anthropic Claude': { - supportsSystemMessage: false, + supportsSystemMessageRole: 'system', supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: false, @@ -153,7 +153,7 @@ const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelN }, 'Llama 3.x': { - supportsSystemMessage: false, + supportsSystemMessageRole: false, supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: false, @@ -161,7 +161,7 @@ const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelN }, 'Deepseek Chat': { - supportsSystemMessage: false, + supportsSystemMessageRole: false, supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: false, @@ -169,7 +169,7 @@ const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelN }, 'Alibaba Qwen2.5 Coder Instruct': { - supportsSystemMessage: false, + supportsSystemMessageRole: false, supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: false, @@ -177,7 +177,7 @@ const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelN }, 'Mistral Codestral': { - supportsSystemMessage: false, + supportsSystemMessageRole: false, supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: false, @@ -185,7 +185,7 @@ const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelN }, 'OpenAI o1, o3': { - supportsSystemMessage: false, + supportsSystemMessageRole: false, supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: false, @@ -193,7 +193,7 @@ const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelN }, 'Deepseek R1': { - supportsSystemMessage: false, + supportsSystemMessageRole: false, supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: false, @@ -201,7 +201,7 @@ const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelN }, '': { - supportsSystemMessage: false, + supportsSystemMessageRole: false, supportsTools: false, supportsAutocompleteFIM: false, supportsStreaming: false, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index b047a601..8d93b0f7 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -5,7 +5,7 @@ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; -import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; +import { anthropicMaxPossibleTokens, developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; import { InternalToolInfo } from '../../common/toolsService.js'; import { addSystemMessageAndToolSupport } from './processMessages.js'; @@ -39,11 +39,14 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: return } - const { messages, separateSystemMessageStr, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: true }) + const { messages, separateSystemMessageStr } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: true }) + + const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) + const { supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); - const tools = devInfo?.supportsTools && (tools_?.length ?? 0) !== 0 ? tools_?.map(tool => toAnthropicTool(tool)) : undefined + const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined const stream = anthropic.messages.stream({ system: separateSystemMessageStr, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 60a66881..86c41a9c 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -8,6 +8,7 @@ import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSe import { Model } from 'openai/resources/models.js'; import { InternalToolInfo } from '../../common/toolsService.js'; import { addSystemMessageAndToolSupport } from './processMessages.js'; +import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; // import { parseMaxTokensStr } from './util.js'; @@ -147,12 +148,14 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { let fullText = '' - const toolCallOfIndex: { [index: string]: { name: string, args: string, id: string } } = {} + const toolCallOfIndex: { [index: string]: { name: string, params: string, id: string } } = {} - const { messages, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: false }) + const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) + const { supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) - const tools = devInfo?.supportsTools && (tools_?.length ?? 0) !== 0 ? tools_?.map(tool => toOpenAITool(tool)) : undefined + const { messages } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: false }) + const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAITool(tool)) : undefined const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { @@ -175,9 +178,9 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: me // tool call for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) { const index = tool.index - if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '', id: '' } + if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', params: '', id: '' } toolCallOfIndex[index].name += tool.function?.name ?? '' - toolCallOfIndex[index].args += tool.function?.arguments ?? ''; + toolCallOfIndex[index].params += tool.function?.arguments ?? ''; toolCallOfIndex[index].id = tool.id ?? '' } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts index f6f6cd5e..2ae792fb 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts @@ -1,7 +1,7 @@ import { LLMChatMessage } from '../../common/llmMessageTypes.js'; -import { DeveloperInfoAtModel, developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js'; +import { developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js'; import { deepClone } from '../../../../../base/common/objects.js'; @@ -9,13 +9,12 @@ import { deepClone } from '../../../../../base/common/objects.js'; // no matter whether the model supports a system message or not (or what format it supports), add it in some way // also take into account tools if the model doesn't support tool use -export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], aiInstructions: string, { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: any[], devInfo: DeveloperInfoAtModel } => { +export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], aiInstructions: string, { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: any[] } => { const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), })) const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const devInfo = developerInfoOfModelName(modelName, overrideSettingsForAllModels) - const { supportsSystemMessage, supportsTools } = devInfo + const { supportsSystemMessageRole: supportsSystemMessage, supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) // 1. SYSTEM MESSAGE // find system messages and concatenate them @@ -236,12 +235,13 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: // TODO!!! + console.log('SYSMG', separateSystemMessage) + console.log('FINAL MESSAGES', finalMessages) return { separateSystemMessageStr, messages: finalMessages, - devInfo, } } From a519100852eb58054e6c19246696b048ea19cd6f Mon Sep 17 00:00:00 2001 From: Joaquin Coromina <75667013+bjoaquinc@users.noreply.github.com> Date: Sun, 16 Feb 2025 23:31:21 +0700 Subject: [PATCH 26/92] Added open-remote-ssh to npm install dirs and the output files in eslint ignore --- .eslintignore | 1 + build/filters.js | 1 + build/npm/dirs.js | 1 + 3 files changed, 3 insertions(+) diff --git a/.eslintignore b/.eslintignore index 0299f2c1..1ddb0330 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,6 +11,7 @@ **/extensions/markdown-language-features/notebook-out/** **/extensions/markdown-math/notebook-out/** **/extensions/notebook-renderers/renderer-out/index.js +**/extensions/open-remote-ssh/out/extension.js **/extensions/simple-browser/media/index.js **/extensions/typescript-language-features/test-workspace/** **/extensions/typescript-language-features/extension.webpack.config.js diff --git a/build/filters.js b/build/filters.js index 4e5049f2..a065d2ed 100644 --- a/build/filters.js +++ b/build/filters.js @@ -134,6 +134,7 @@ module.exports.indentationFilter = [ '!extensions/markdown-math/notebook-out/*.js', '!extensions/ipynb/notebook-out/**', '!extensions/notebook-renderers/renderer-out/*.js', + '!extensions/open-remote-ssh/out/*.js', '!extensions/simple-browser/media/*.js', ]; diff --git a/build/npm/dirs.js b/build/npm/dirs.js index 7b11bde9..d06a1ec1 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -36,6 +36,7 @@ const dirs = [ 'extensions/microsoft-authentication', 'extensions/notebook-renderers', 'extensions/npm', + 'extensions/open-remote-ssh', 'extensions/php-language-features', 'extensions/references-view', 'extensions/search-result', From 249eee341d8b745bae6946783326e9aa24bc7f8e Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 16 Feb 2025 00:13:26 -0800 Subject: [PATCH 27/92] better context and file reading --- .../contrib/void/browser/chatThreadService.ts | 206 +++++++++++------- .../contrib/void/browser/helpers/readFile.ts | 39 +++- .../contrib/void/browser/prompt/prompts.ts | 64 ++++-- .../react/src/markdown/ChatMarkdownRender.tsx | 1 - .../react/src/sidebar-tsx/SidebarChat.tsx | 65 +++--- .../browser/react/src/sidebar-tsx/delete.tsx | 1 + .../contrib/void/browser/sidebarActions.ts | 17 +- .../contrib/void/common/toolsService.ts | 4 +- 8 files changed, 265 insertions(+), 132 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index a3452eb2..248ab441 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -13,7 +13,9 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; +import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles } from './prompt/prompts.js'; +import { LLMChatMessage } from '../common/llmMessageTypes.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { @@ -32,23 +34,17 @@ export type FileSelection = { export type StagingSelectionItem = CodeSelection | FileSelection - -export type StagingInfo = { - isBeingEdited: boolean; - selections: StagingSelectionItem[] | null; // staging selections in edit mode -} - -const defaultStaging: StagingInfo = { isBeingEdited: false, selections: [] } - - // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. export type ChatMessage = | { role: 'user'; - content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty) + content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user - allowed to be '', will be ignored selections: StagingSelectionItem[] | null; // the user's selection - staging: StagingInfo | null + state: { + stagingSelections: StagingSelectionItem[]; + isBeingEdited: boolean; + } } | { role: 'assistant'; @@ -61,6 +57,11 @@ export type ChatMessage = displayContent?: undefined; } +type UserMessageType = ChatMessage & { role: 'user' } +type UserMessageState = UserMessageType['state'] + +export const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false } + // a 'thread' means a chat message history export type ChatThreads = { [id: string]: { @@ -68,11 +69,18 @@ export type ChatThreads = { createdAt: string; // ISO string lastModified: string; // ISO string messages: ChatMessage[]; - staging: StagingInfo | null; - focusedMessageIdx?: number | undefined; // index of the message that is being edited (undefined if none) + state: { + stagingSelections: StagingSelectionItem[]; + focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none) + isCheckedOfSelectionId: { [selectionId: string]: boolean }; + } }; } +type ThreadType = ChatThreads[string] + +const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} } + export type ThreadsState = { allThreads: ChatThreads; currentThreadId: string; // intended for internal use only @@ -94,11 +102,12 @@ const newThreadObject = () => { createdAt: now, lastModified: now, messages: [], - focusedMessageIdx: undefined, - staging: { - isBeingEdited: true, - selections: [], - } + state: { + stagingSelections: [], + focusedMessageIdx: undefined, + isCheckedOfSelectionId: {} + }, + } satisfies ChatThreads[string] } @@ -124,7 +133,9 @@ export interface IChatThreadService { isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; - _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; + // _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; + _useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial) => void]; + _useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial) => void]; editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise; addUserMessageAndStreamResponse(userMessage: string): Promise; @@ -150,6 +161,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { constructor( @IStorageService private readonly _storageService: IStorageService, @IModelService private readonly _modelService: IModelService, + @IFileService private readonly _fileService: IFileService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, ) { super() @@ -190,21 +202,19 @@ class ChatThreadService extends Disposable implements IChatThreadService { const threads: ChatThreads = oldThreadsObject /** v1 -> v2 - - threadsState.currentStagingSelections: CodeStagingSelection[] | null; - + thread.staging: StagingInfo - + thread.focusedMessageIdx?: number | undefined; - - + chatMessage.staging: StagingInfo | null - */ + - threads.state.currentStagingSelections: CodeStagingSelection[] | null; + + thread[threadIdx].state + + message.state +*/ // check if we need to update let shouldUpdate = false for (const thread of Object.values(threads)) { - if (!thread.staging) { + if (!thread.state) { shouldUpdate = true } for (const chatMessage of Object.values(thread.messages)) { - if (chatMessage.role === 'user' && !chatMessage.staging) { + if (chatMessage.role === 'user' && !chatMessage.state) { shouldUpdate = true } } @@ -214,13 +224,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { // update the threads for (const thread of Object.values(threads)) { - if (!thread.staging) { - thread.staging = defaultStaging - thread.focusedMessageIdx = undefined + if (!thread.state) { + thread.state = defaultThreadState } for (const chatMessage of Object.values(thread.messages)) { - if (chatMessage.role === 'user' && !chatMessage.staging) { - chatMessage.staging = defaultStaging + if (chatMessage.role === 'user' && !chatMessage.state) { + chatMessage.state = defaultMessageState } } } @@ -245,6 +254,17 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._onDidChangeCurrentThread.fire() } + private _getAllSelections() { + const thread = this.getCurrentThread() + return thread.messages.flatMap(m => m.role === 'user' && m.selections || []) + } + + private _getSelectionsUpToMessageIdx(messageIdx: number) { + const thread = this.getCurrentThread() + const prevMessages = thread.messages.slice(0, messageIdx) + return prevMessages.flatMap(m => m.role === 'user' && m.selections || []) + } + private _setStreamState(threadId: string, state: Partial>) { this.streamState[threadId] = { ...this.streamState[threadId], @@ -268,12 +288,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.getCurrentThread() - const messageToReplace = thread.messages[messageIdx] - if (messageToReplace?.role !== 'user') { - console.log(`Error: tried to edit non-user message. messageIdx=${messageIdx}, numMessages=${thread.messages.length}`) - return + if (thread.messages?.[messageIdx]?.role !== 'user') { + throw new Error("Error: editing a message with role !=='user'") } + // get prev and curr selections before clearing the message + const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx) + const currSelns = thread.messages[messageIdx].selections || [] + // clear messages up to the index const slicedMessages = thread.messages.slice(0, messageIdx) this._setState({ @@ -287,36 +309,45 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, true) // stream the edit - this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging) + this.addUserMessageAndStreamResponse(userMessage, { prevSelns, currSelns }) } - async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { - + async addUserMessageAndStreamResponse(userMessage: string, options?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] }) { const thread = this.getCurrentThread() const threadId = thread.id - let threadStaging = thread.staging - - const currStaging = stagingOverride ?? threadStaging ?? defaultStaging // don't use _useFocusedStagingState to avoid race conditions with focusing - const { selections: currSelns, } = currStaging - // add user's message to chat history const instructions = userMessage - const content = await chat_userMessage(instructions, currSelns, this._modelService) - const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, staging: null, } + + const prevSelns: StagingSelectionItem[] = options?.prevSelns ?? this._getAllSelections() + const currSelns: StagingSelectionItem[] = options?.currSelns ?? thread.state.stagingSelections + + // read all curr+previous files on demand instead of adding them to the history + const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns) + const messageContentWithAllFiles = await chat_userMessageContentWithAllFiles(instructions, prevSelns, currSelns, this._modelService, this._fileService) + const prevLLMMessages = this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })) + const currLLMMessage: LLMChatMessage = { role: 'user', content: messageContentWithAllFiles } + + const userHistoryElt: ChatMessage = { role: 'user', content: messageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) this._setStreamState(threadId, { error: undefined }) + console.log(`messageContent`) + console.log([{ role: 'system', content: chat_systemMessage }, + ...prevLLMMessages, + currLLMMessage,]) + const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', logging: { loggingName: 'Chat' }, useProviderFor: 'Ctrl+L', messages: [ { role: 'system', content: chat_systemMessage }, - ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), + ...prevLLMMessages, + currLLMMessage, ], onText: ({ newText, fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) @@ -357,13 +388,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.getCurrentThread() // get the focusedMessageIdx - const focusedMessageIdx = thread.focusedMessageIdx + const focusedMessageIdx = thread.state.focusedMessageIdx if (focusedMessageIdx === undefined) return; // check that the message is actually being edited const focusedMessage = thread.messages[focusedMessageIdx] if (focusedMessage.role !== 'user') return; - if (!focusedMessage.staging?.isBeingEdited) return; + if (!focusedMessage.state) return; return focusedMessageIdx } @@ -429,28 +460,34 @@ class ChatThreadService extends Disposable implements IChatThreadService { ...this.state.allThreads, [threadId]: { ...thread, - focusedMessageIdx: messageIdx + state: { + ...thread.state, + focusedMessageIdx: messageIdx, + } } } }, true) } - // set thread.messages[messageIdx].stagingSelections - private setEditMessageStaging(staging: StagingInfo, messageIdx: number): void { + // set message.state + private _setCurrentMessageState(state: Partial, messageIdx: number): void { - const thread = this.getCurrentThread() - const message = thread.messages[messageIdx] - if (message.role !== 'user') return; + const threadId = this.state.currentThreadId + const thread = this.state.allThreads[threadId] + if (!thread) return this._setState({ allThreads: { ...this.state.allThreads, - [thread.id]: { + [threadId]: { ...thread, messages: thread.messages.map((m, i) => - i === messageIdx ? { + i === messageIdx && m.role === 'user' ? { ...m, - staging, + state: { + ...m.state, + ...state + }, } : m ) } @@ -459,48 +496,53 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - // set thread.stagingSelections - private setDefaultStaging(staging: StagingInfo): void { + // set thread.state + private _setCurrentThreadState(state: Partial): void { - const thread = this.getCurrentThread() + const threadId = this.state.currentThreadId + const thread = this.state.allThreads[threadId] + if (!thread) return this._setState({ allThreads: { ...this.state.allThreads, [thread.id]: { ...thread, - staging, + state: { + ...thread.state, + ...state + } } } }, true) } - // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) - _useFocusedStagingState(messageIdx?: number | undefined) { - const defaultStaging = { isBeingEdited: false, selections: [], text: '' } - - let staging: StagingInfo = defaultStaging - let setStaging: (selections: StagingInfo) => void = () => { } + _useCurrentMessageState(messageIdx: number) { const thread = this.getCurrentThread() - const isFocusingMessage = messageIdx !== undefined - if (isFocusingMessage) { // is editing message + const messages = thread.messages + const currMessage = messages[messageIdx] - const message = thread.messages[messageIdx!] - if (message.role === 'user') { - staging = message.staging || defaultStaging - setStaging = (s) => this.setEditMessageStaging(s, messageIdx) - } - - } - else { // is editing the default input box - staging = thread.staging || defaultStaging - setStaging = this.setDefaultStaging.bind(this) + if (currMessage.role !== 'user') { + return [defaultMessageState, (s: any) => { }] as const } - return [staging, setStaging] as const + const state = currMessage.state + const setState = (newState: Partial) => this._setCurrentMessageState(newState, messageIdx) + + return [state, setState] as const + + } + + _useCurrentThreadState() { + const thread = this.getCurrentThread() + + const state = thread.state + const setState = this._setCurrentThreadState.bind(this) + + return [state, setState] as const } diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts index b0f154d1..f7752b84 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -3,14 +3,41 @@ import { EndOfLinePreference } from '../../../../../editor/common/model' import { IModelService } from '../../../../../editor/common/services/model.js' import { IFileService } from '../../../../../platform/files/common/files' -// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.) -export const VSReadFile = async (modelService: IModelService, uri: URI): Promise => { - const model = modelService.getModel(uri) - if (!model) return null - return model.getValue(EndOfLinePreference.LF) + +// attempts to read URI of currently opened model, then of raw file +export const VSReadFile = async (modelService: IModelService, fileService: IFileService, uri: URI) => { + + const modelResult = await _VSReadModel(modelService, uri) + if (modelResult) return modelResult + + const fileResult = await _VSReadFileRaw(fileService, uri) + if (fileResult) return fileResult + + return '' + } -export const VSReadFileRaw = async (fileService: IFileService, uri: URI) => { +// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.) +export const _VSReadModel = async (modelService: IModelService, uri: URI): Promise => { + + // attempt to read saved model (sometimes doesn't work if page is reloaded) + const model = modelService.getModel(uri) + if (model) { + return model.getValue(EndOfLinePreference.LF) + } + + // look at all opened models and check if they have the same `fsPath` + const models = modelService.getModels(); + for (const model of models) { + if (model.uri.fsPath.toString() === uri.fsPath.toString()) { + return model.getValue(EndOfLinePreference.LF); + } + } + + return null +} + +export const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => { const res = await fileService.readFile(uri) const str = res.value.toString() return str diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index b3fb4482..5ecb924a 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -7,8 +7,9 @@ import { URI } from '../../../../../base/common/uri.js'; import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; -import { VSReadFile } from '../helpers/readFile.js'; +import { _VSReadModel, VSReadFile } from '../helpers/readFile.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; // this is just for ease of readability @@ -156,10 +157,10 @@ ${tripleTick[1]} } const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.' -const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService) => { +const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => { if (fileSelections.length === 0) return null const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => { - const content = await VSReadFile(modelService, sel.fileURI) ?? failToReadStr + const content = await VSReadFile(modelService, fileService, sel.fileURI) ?? failToReadStr return { ...sel, content } })) return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') @@ -167,23 +168,60 @@ const stringifyFileSelections = async (fileSelections: FileSelection[], modelSer const stringifyCodeSelections = (codeSelections: CodeSelection[]) => { return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') } +const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => { + if (!currSelns) return '' + return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n') +} +export const chat_userMessageContent = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null) => { - -export const chat_userMessage = async (instructions: string, selections: StagingSelectionItem[] | null, modelService: IModelService) => { - const fileSelections = selections?.filter(s => s.type === 'File') as FileSelection[] - const codeSelections = selections?.filter(s => s.type === 'Selection') as CodeSelection[] - - const filesStr = await stringifyFileSelections(fileSelections, modelService) - const codeStr = stringifyCodeSelections(codeSelections) + const selnsStr = stringifySelectionNames(currSelns) let str = '' - if (filesStr) str += `FILES\n${filesStr}\n` - if (codeStr) str += `SELECTIONS\n${codeStr}\n` - str += `INSTRUCTIONS\n${instructions}` + if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` } + str += `\nINSTRUCTIONS\n${instructions}` return str; }; +export const chat_userMessageContentWithAllFilesToo = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => { + + // ADD IN FILES AT TOP + const allSelections = [...currSelns || [], ...prevSelns || []] + + const codeSelections: CodeSelection[] = [] + const fileSelections: FileSelection[] = [] + const filesURIs = new Set() + + for (const selection of allSelections) { + if (selection.type === 'Selection') { + codeSelections.push(selection) + } + else if (selection.type === 'File') { + const fileSelection = selection + const path = fileSelection.fileURI.fsPath + if (!filesURIs.has(path)) { + filesURIs.add(path) + fileSelections.push(fileSelection) + } + } + } + + const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService) + const selnsStr = stringifyCodeSelections(codeSelections) + + // ACTUAL MESSAGE CONTENT + const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns) + + + let str = '' + + str += 'ALL FILE CONTENTS\n' + if (filesStr) str += `${filesStr}\n` + if (selnsStr) str += `${selnsStr}\n` + if (messageContent) str += `\n${messageContent}\n` + + return str; +}; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 86afcc33..351a399a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -92,7 +92,6 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok // deal with built-in tokens first (assume marked token) const t = token as MarkedToken - console.log(t.raw) if (t.type === "space") { return {t.raw} 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 2aaf9dd2..52944476 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 @@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js'; -import { ChatMessage, StagingInfo, StagingSelectionItem } from '../../../chatThreadService.js'; +import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; @@ -156,8 +156,8 @@ interface VoidChatAreaProps { showSelections?: boolean; showProspectiveSelections?: boolean; - staging?: StagingInfo - setStaging?: (s: StagingInfo) => void + selections?: StagingSelectionItem[] + setSelections?: (s: StagingSelectionItem[]) => void // selections?: any[]; // onSelectionsChange?: (selections: any[]) => void; @@ -180,8 +180,8 @@ export const VoidChatArea: React.FC = ({ featureName, showSelections = false, showProspectiveSelections = true, - staging, - setStaging, + selections, + setSelections, }) => { return (
= ({ }} > {/* Selections section */} - {showSelections && staging && setStaging && ( + {showSelections && selections && setSelections && ( setStaging({ ...staging, selections })} + selections={selections} + setSelections={setSelections} showProspectiveSelections={showProspectiveSelections} /> )} @@ -550,9 +550,23 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - // edit mode state - const [staging, setStaging] = chatThreadsService._useFocusedStagingState(messageIdx) - const mode: ChatBubbleMode = staging.isBeingEdited ? 'edit' : 'display' + // global state + let isBeingEdited = false + let setIsBeingEdited = (v: boolean) => { } + let stagingSelections: StagingSelectionItem[] = [] + let setStagingSelections = (s: StagingSelectionItem[]) => { } + + if (messageIdx !== undefined) { + const [_state, _setState] = chatThreadsService._useCurrentMessageState(messageIdx) + isBeingEdited = _state.isBeingEdited + setIsBeingEdited = (v) => _setState({ isBeingEdited: v }) + stagingSelections = _state.stagingSelections + setStagingSelections = (s) => { _setState({ stagingSelections: s }) } + } + + + // local state + const mode: ChatBubbleMode = isBeingEdited ? 'edit' : 'display' const [isFocused, setIsFocused] = useState(false) const [isHovered, setIsHovered] = useState(false) const [isDisabled, setIsDisabled] = useState(false) @@ -565,10 +579,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current if (canInitialize && shouldInitialize) { - setStaging({ - ...staging, - selections: chatMessage.selections || [], - }) + setStagingSelections(chatMessage.selections || []) + if (textAreaFnsRef.current) textAreaFnsRef.current.setValue(chatMessage.displayContent || '') @@ -581,14 +593,14 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM }, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) const EditSymbol = mode === 'display' ? Pencil : X const onOpenEdit = () => { - setStaging({ ...staging, isBeingEdited: true }) + setIsBeingEdited(true) chatThreadsService.setFocusedMessageIdx(messageIdx) _justEnabledEdit.current = true } const onCloseEdit = () => { setIsFocused(false) setIsHovered(false) - setStaging({ ...staging, isBeingEdited: false }) + setIsBeingEdited(false) chatThreadsService.setFocusedMessageIdx(undefined) } @@ -614,7 +626,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatThreadsService.cancelStreaming(thread.id) // reset state - setStaging({ ...staging, isBeingEdited: false }) + setIsBeingEdited(false) chatThreadsService.setFocusedMessageIdx(undefined) // stream the edit @@ -649,8 +661,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM showSelections={true} showProspectiveSelections={false} featureName="Ctrl+L" - staging={staging} - setStaging={setStaging} + selections={stagingSelections} + setSelections={setStagingSelections} > { const currentThread = chatThreadsService.getCurrentThread() const previousMessages = currentThread?.messages ?? [] - const [staging, setStaging] = chatThreadsService._useFocusedStagingState() + + const [_state, _setState] = chatThreadsService._useCurrentThreadState() + const selections = _state.stagingSelections + const setSelections = (s: StagingSelectionItem[]) => { _setState({ stagingSelections: s }) } // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) @@ -797,11 +812,11 @@ export const SidebarChat = () => { const userMessage = textAreaRef.current?.value ?? '' await chatThreadsService.addUserMessageAndStreamResponse(userMessage) - setStaging({ ...staging, selections: [], }) // clear staging + setSelections([]) // clear staging textAreaFnsRef.current?.setValue('') textAreaRef.current?.focus() // focus input after submit - }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, staging, setStaging]) + }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, selections, setSelections]) const onAbort = () => { const threadId = currentThread.id @@ -887,8 +902,8 @@ export const SidebarChat = () => { isDisabled={isDisabled} showSelections={true} showProspectiveSelections={prevMessagesHTML.length === 0} - staging={staging} - setStaging={setStaging} + selections={selections} + setSelections={setSelections} onClickAnywhere={() => { textAreaRef.current?.focus() }} featureName="Ctrl+L" > diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx new file mode 100644 index 00000000..07a09338 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx @@ -0,0 +1 @@ +A B C diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index d65c51a7..2e64c53f 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -135,9 +135,20 @@ registerAction2(class extends Action2 { const chatThreadService = accessor.get(IChatThreadService) const focusedMessageIdx = chatThreadService.getFocusedMessageIdx() - const [staging, setStaging] = chatThreadService._useFocusedStagingState(focusedMessageIdx) - const selections = staging.selections || [] - const setSelections = (s: StagingSelectionItem[]) => setStaging({ ...staging, selections: s }) + + // set the selections to the proper value + let selections: StagingSelectionItem[] = [] + let setSelections = (s: StagingSelectionItem[]) => { } + + if (focusedMessageIdx === undefined) { + const [state, setState] = chatThreadService._useCurrentThreadState() + selections = state.stagingSelections + setSelections = (s) => setState({ stagingSelections: s }) + } else { + const [state, setState] = chatThreadService._useCurrentMessageState(focusedMessageIdx) + selections = state.stagingSelections + setSelections = (s) => setState({ stagingSelections: s }) + } // if matches with existing selection, overwrite (since text may change) const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection) diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 8ffd6b9b..e96186c9 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -4,7 +4,7 @@ import { IFileService, IFileStat } from '../../../../platform/files/common/files import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js' -import { VSReadFileRaw } from '../../../../workbench/contrib/void/browser/helpers/readFile.js' +import { _VSReadFileRaw } from '../../../../workbench/contrib/void/browser/helpers/readFile.js' import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js' import { ISearchService } from '../../../../workbench/services/search/common/search.js' @@ -140,7 +140,7 @@ export class ToolService implements IToolService { this.contextToolCallFns = { read_file: async ({ uri: uriStr }) => { const uri = validateURI(uriStr) - const fileContents = await VSReadFileRaw(fileService, uri) + const fileContents = await _VSReadFileRaw(fileService, uri) return fileContents ?? '(could not read file)' }, list_dir: async ({ uri: uriStr }) => { From 36b1b56690870b1d99565e44837fc867ff672aa6 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 16 Feb 2025 00:53:11 -0800 Subject: [PATCH 28/92] bug --- .../contrib/void/browser/react/src/sidebar-tsx/delete.tsx | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx deleted file mode 100644 index 07a09338..00000000 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx +++ /dev/null @@ -1 +0,0 @@ -A B C From 41fe5c50e2567fd58083bf52e26c30530df6aec0 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 16 Feb 2025 01:48:25 -0800 Subject: [PATCH 29/92] fix scrollbars --- .../react/src/util/useScrollbarStyles.tsx | 188 ++++++++++-------- 1 file changed, 109 insertions(+), 79 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx index 7f8ceb34..94df3aac 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx @@ -1,7 +1,6 @@ import { useEffect } from 'react'; export const useScrollbarStyles = (containerRef: React.MutableRefObject) => { - useEffect(() => { if (!containerRef.current) return; @@ -12,90 +11,121 @@ export const useScrollbarStyles = (containerRef: React.MutableRefObject { + // Get all matching elements within the container, including the container itself + const scrollElements = [ + ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []), + ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || []) + ]; - // Apply styles and listeners to each scroll element - scrollElements.forEach(element => { - // Add the scrollable class directly to the overflow element - element.classList.add('void-scrollable-element'); - - let fadeTimeout: NodeJS.Timeout | null = null; - let fadeInterval: NodeJS.Timeout | null = null; - - const fadeIn = () => { - if (fadeInterval) clearInterval(fadeInterval); - - let step = 0; - fadeInterval = setInterval(() => { - if (step <= 10) { - element.classList.remove(`show-scrollbar-${step - 1}`); - element.classList.add(`show-scrollbar-${step}`); - step++; - } else { - clearInterval(fadeInterval!); - } - }, 10); - }; - - const fadeOut = () => { - if (fadeInterval) clearInterval(fadeInterval); - - let step = 10; - fadeInterval = setInterval(() => { - if (step >= 0) { - element.classList.remove(`show-scrollbar-${step + 1}`); - element.classList.add(`show-scrollbar-${step}`); - step--; - } else { - clearInterval(fadeInterval!); - } - }, 60); - }; - - const onMouseEnter = () => { - if (fadeTimeout) clearTimeout(fadeTimeout); - if (fadeInterval) clearInterval(fadeInterval); - fadeIn(); - }; - - const onMouseLeave = () => { - if (fadeTimeout) clearTimeout(fadeTimeout); - fadeTimeout = setTimeout(() => { - fadeOut(); - }, 10); - }; - - element.addEventListener('mouseenter', onMouseEnter); - element.addEventListener('mouseleave', onMouseLeave); - - // Store cleanup function - const cleanup = () => { - element.removeEventListener('mouseenter', onMouseEnter); - element.removeEventListener('mouseleave', onMouseLeave); - if (fadeTimeout) clearTimeout(fadeTimeout); - if (fadeInterval) clearInterval(fadeInterval); - element.classList.remove('void-scrollable-element'); - // Remove any remaining show-scrollbar classes - for (let i = 0; i <= 10; i++) { - element.classList.remove(`show-scrollbar-${i}`); - } - }; - - // Store the cleanup function on the element for later use - (element as any).__scrollbarCleanup = cleanup; - }); - - return () => { - // Clean up all scroll elements + // Clean up existing elements first scrollElements.forEach(element => { if ((element as any).__scrollbarCleanup) { (element as any).__scrollbarCleanup(); } }); + + // Apply styles and listeners to each scroll element + scrollElements.forEach(element => { + // Add the scrollable class directly to the overflow element + element.classList.add('void-scrollable-element'); + + let fadeTimeout: NodeJS.Timeout | null = null; + let fadeInterval: NodeJS.Timeout | null = null; + + const fadeIn = () => { + if (fadeInterval) clearInterval(fadeInterval); + + let step = 0; + fadeInterval = setInterval(() => { + if (step <= 10) { + element.classList.remove(`show-scrollbar-${step - 1}`); + element.classList.add(`show-scrollbar-${step}`); + step++; + } else { + clearInterval(fadeInterval!); + } + }, 10); + }; + + const fadeOut = () => { + if (fadeInterval) clearInterval(fadeInterval); + + let step = 10; + fadeInterval = setInterval(() => { + if (step >= 0) { + element.classList.remove(`show-scrollbar-${step + 1}`); + element.classList.add(`show-scrollbar-${step}`); + step--; + } else { + clearInterval(fadeInterval!); + } + }, 60); + }; + + const onMouseEnter = () => { + if (fadeTimeout) clearTimeout(fadeTimeout); + if (fadeInterval) clearInterval(fadeInterval); + fadeIn(); + }; + + const onMouseLeave = () => { + if (fadeTimeout) clearTimeout(fadeTimeout); + fadeTimeout = setTimeout(() => { + fadeOut(); + }, 10); + }; + + element.addEventListener('mouseenter', onMouseEnter); + element.addEventListener('mouseleave', onMouseLeave); + + // Store cleanup function + const cleanup = () => { + element.removeEventListener('mouseenter', onMouseEnter); + element.removeEventListener('mouseleave', onMouseLeave); + if (fadeTimeout) clearTimeout(fadeTimeout); + if (fadeInterval) clearInterval(fadeInterval); + element.classList.remove('void-scrollable-element'); + // Remove any remaining show-scrollbar classes + for (let i = 0; i <= 10; i++) { + element.classList.remove(`show-scrollbar-${i}`); + } + }; + + // Store the cleanup function on the element for later use + (element as any).__scrollbarCleanup = cleanup; + }); + }; + + // Initialize for the first time + initializeScrollbarStyles(); + + // Set up mutation observer + const observer = new MutationObserver((mutations) => { + initializeScrollbarStyles(); + }); + + // Start observing the container for child changes + observer.observe(containerRef.current, { + childList: true, + subtree: true + }); + + return () => { + observer.disconnect(); + // Your existing cleanup code... + if (containerRef.current) { + const scrollElements = [ + ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []), + ...Array.from(containerRef.current.querySelectorAll(overflowSelector)) + ]; + scrollElements.forEach(element => { + if ((element as any).__scrollbarCleanup) { + (element as any).__scrollbarCleanup(); + } + }); + } }; }, [containerRef]); }; From 2bc3d67e39720aaa8bbf9db5f20ecb42112a90a5 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 16 Feb 2025 19:51:58 -0800 Subject: [PATCH 30/92] minor tool use fix --- src/vs/workbench/contrib/void/browser/chatThreadService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 4565b6a6..81e4a434 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -326,12 +326,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { messageSoFar: fullText }) }, onFinalMessage: async ({ fullText, tools }) => { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText, tool_calls: tools }) if ((tools?.length ?? 0) === 0) { this._finishStreamingTextMessage(threadId, fullText) } else { + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText, tool_calls: tools }) for (const tool of tools ?? []) { if (!(tool.name in this._toolsService.toolFns)) { this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.params, id: tool.id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) From 74f8303803c6308770e0d525004f5b6decc64da2 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 16 Feb 2025 20:11:13 -0800 Subject: [PATCH 31/92] discard star changes --- .../browser/parts/editor/editorActions.ts | 20 +--------------- .../editor/media/multieditortabscontrol.css | 18 +++++++++++---- .../parts/editor/multiEditorTabsControl.ts | 23 +++++++++---------- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 39c4275b..1b1a39b3 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -432,7 +432,7 @@ export class UnpinEditorAction extends Action { label: string, @ICommandService private readonly commandService: ICommandService ) { - super(id, label, ThemeIcon.asClassName(Codicon.starFull)); + super(id, label, ThemeIcon.asClassName(Codicon.pinned)); } override run(context?: IEditorCommandsContext): Promise { @@ -440,24 +440,6 @@ export class UnpinEditorAction extends Action { } } -export class PinEditorAction extends Action { - - static readonly ID = 'workbench.action.pinEditor'; - static readonly LABEL = localize('pinEditor', "Pin Editor"); - - constructor( - id: string, - label: string, - @ICommandService private readonly commandService: ICommandService - ) { - super(id, label, ThemeIcon.asClassName(Codicon.star)); - } - - override async run(context?: IEditorCommandsContext): Promise { - return this.commandService.executeCommand('workbench.action.pinEditor', undefined, context); - } -} - export class CloseEditorTabAction extends Action { static readonly ID = 'workbench.action.closeActiveEditor'; diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index dd6c233f..93559402 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -385,7 +385,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-fixed > .tab-actions { flex: 0; - overflow: visible; /* ensure tab actions are always visible */ + overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink/fixed to make more room */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions, @@ -399,8 +399,18 @@ overflow: visible; /* ...but still show the tab actions on hover, focus and when dirty or sticky */ } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-action-off:not(.dirty) > .tab-actions, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky-compact > .tab-actions { - display: none; /* only hide tab actions when sticky-compact */ + display: none; /* hide the tab actions when we are configured to hide it (unless dirty, but always when sticky-compact) */ +} + +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-actions .action-label, /* always show tab actions for active tab */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-actions .action-label:focus, /* always show tab actions on focus */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-actions .action-label, /* always show tab actions on hover */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-actions .action-label, /* always show tab actions on hover */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, /* always show tab actions for sticky tabs */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-actions .action-label { /* always show tab actions for dirty tabs */ + opacity: 1; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .actions-container { @@ -434,11 +444,11 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-actions .action-label { - opacity: 1; + opacity: 0.5; /* show tab actions dimmed for inactive group */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .action-label { - opacity: 1; + opacity: 0; } /* Tab Actions: Off */ diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index bbda32c4..b23be82e 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -35,7 +35,7 @@ import { MergeGroupMode, IMergeGroupOptions } from '../../../services/editor/com import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from './editor.js'; -import { CloseEditorTabAction, PinEditorAction, UnpinEditorAction } from './editorActions.js'; +import { CloseEditorTabAction, UnpinEditorAction } from './editorActions.js'; import { assertAllDefined, assertIsDefined } from '../../../../base/common/types.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { basenameOrAuthority } from '../../../../base/common/resources.js'; @@ -113,7 +113,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseEditorTabAction, CloseEditorTabAction.ID, CloseEditorTabAction.LABEL)); private readonly unpinEditorAction = this._register(this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL)); - private readonly pinEditorAction = this._register(this.instantiationService.createInstance(PinEditorAction, PinEditorAction.ID, PinEditorAction.LABEL)); // Add this line private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); private tabLabels: IEditorInputLabel[] = []; @@ -1519,28 +1518,28 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel); // Action - const hasCloseAction = options.tabActionCloseVisibility; - const hasAction = true; // Always show actions + const hasUnpinAction = isTabSticky && options.tabActionUnpinVisibility; + const hasCloseAction = !hasUnpinAction && options.tabActionCloseVisibility; + const hasAction = hasUnpinAction || hasCloseAction; - // Determine which action to show let tabAction; - if (isTabSticky) { - tabAction = this.unpinEditorAction; + if (hasAction) { + tabAction = hasUnpinAction ? this.unpinEditorAction : this.closeEditorAction; } else { - tabAction = this.pinEditorAction; // Use pin action instead of close action + // Even if the action is not visible, add it as it contains the dirty indicator + tabAction = isTabSticky ? this.unpinEditorAction : this.closeEditorAction; } - // Update action bar if (!tabActionBar.hasAction(tabAction)) { if (!tabActionBar.isEmpty()) { tabActionBar.clear(); } + tabActionBar.push(tabAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(tabAction) }); } - tabContainer.classList.toggle('sticky', isTabSticky); - tabContainer.classList.toggle(`pinned-action-off`, false); - tabContainer.classList.toggle(`close-action-off`, !hasCloseAction); + tabContainer.classList.toggle(`pinned-action-off`, isTabSticky && !hasUnpinAction); + tabContainer.classList.toggle(`close-action-off`, !hasUnpinAction && !hasCloseAction); for (const option of ['left', 'right']) { tabContainer.classList.toggle(`tab-actions-${option}`, hasAction && options.tabActionLocation === option); From 432a1766afce98b2e596d4da271a2480cec1755c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 16 Feb 2025 20:50:07 -0800 Subject: [PATCH 32/92] anthropic tool use fix --- .../contrib/void/browser/chatThreadService.ts | 24 +++++++------ .../contrib/void/common/llmMessageTypes.ts | 17 +++++++--- .../contrib/void/common/toolsService.ts | 2 +- .../electron-main/llmMessage/anthropic.ts | 6 ++-- .../void/electron-main/llmMessage/openai.ts | 9 +++-- ...ssMessages.ts => preprocessLLMMessages.ts} | 34 +++++++++++++------ .../llmMessage/sendLLMMessage.ts | 8 ++--- .../void/electron-main/llmMessageChannel.ts | 2 +- 8 files changed, 65 insertions(+), 37 deletions(-) rename src/vs/workbench/contrib/void/electron-main/llmMessage/{processMessages.ts => preprocessLLMMessages.ts} (88%) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 81e4a434..03286d38 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -46,24 +46,25 @@ const defaultStaging: StagingInfo = { isBeingEdited: false, selections: [] } // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. export type ChatMessage = | { + role: 'system'; + content: string; + displayContent?: undefined; + } | { role: 'user'; content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user - allowed to be '', will be ignored selections: StagingSelectionItem[] | null; // the user's selection staging: StagingInfo | null - } - | { + } | { role: 'assistant'; - tool_calls?: { name: string, id: string, params: string }[]; + tool_calls?: { + name: string, + id: string, + params: string + }[]; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored - } - | { - role: 'system'; - content: string; - displayContent?: undefined; - } - | { + } | { role: 'tool'; name: string; // internal use params: string; // internal use @@ -325,7 +326,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { onText: ({ fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) }, - onFinalMessage: async ({ fullText, tools }) => { + onFinalMessage: async ({ fullText, toolCalls: tools }) => { + console.log('FINAL MESSAGE', fullText, tools) if ((tools?.length ?? 0) === 0) { this._finishStreamingTextMessage(threadId, fullText) diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index e82da2cb..79960826 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -22,10 +22,6 @@ export const errorDetails = (fullError: Error | null): string | null => { return null } -export type OnText = (p: { newText: string, fullText: string }) => void -export type OnFinalMessage = (p: { fullText: string, tools?: { name: string, params: string, id: string, }[] }) => void // id is tool_use_id -export type OnError = (p: { message: string, fullError: Error | null }) => void -export type AbortRef = { current: (() => void) | null } export type LLMChatMessage = { role: 'system' | 'user'; @@ -41,6 +37,19 @@ export type LLMChatMessage = { id: string; } +export type LLMToolCallType = { + name: string; + params: string; + id: string; +} + + +export type OnText = (p: { newText: string, fullText: string }) => void +export type OnFinalMessage = (p: { fullText: string, toolCalls?: LLMToolCallType[] }) => void // id is tool_use_id +export type OnError = (p: { message: string, fullError: Error | null }) => void +export type AbortRef = { current: (() => void) | null } + + export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { if (c.role === 'system' || c.role === 'user') { return { role: c.role, content: c.content ?? '(empty)' } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 32004ff2..0c7c7f82 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -142,7 +142,7 @@ export class ToolsService implements IToolsService { const queryBuilder = instantiationService.createInstance(QueryBuilder); - const parseObj = (s: string): { [s: string]: unknown } | null => { + const parseObj = (s: string): { [s: string]: unknown } | null => { try { const o = JSON.parse(s) return o diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index 8d93b0f7..fec9dc07 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -7,7 +7,7 @@ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { anthropicMaxPossibleTokens, developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; import { InternalToolInfo } from '../../common/toolsService.js'; -import { addSystemMessageAndToolSupport } from './processMessages.js'; +import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; @@ -86,9 +86,9 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: stream.on('finalMessage', (response) => { // stringify the response's content const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - // const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input), tool_use_id: c.id } : null).filter(c => !!c) + const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null).filter(c => !!c) - onFinalMessage({ fullText: content, tools: [] }) + onFinalMessage({ fullText: content, toolCalls: tools }) }) stream.on('error', (error) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 86c41a9c..49dd0bfd 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -7,7 +7,7 @@ import OpenAI from 'openai'; import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { Model } from 'openai/resources/models.js'; import { InternalToolInfo } from '../../common/toolsService.js'; -import { addSystemMessageAndToolSupport } from './processMessages.js'; +import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; // import { parseMaxTokensStr } from './util.js'; @@ -192,7 +192,12 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: me onText({ newText, fullText }); } - onFinalMessage({ fullText, tools: Object.keys(toolCallOfIndex).map(index => toolCallOfIndex[index]) }); + onFinalMessage({ + fullText, toolCalls: Object.keys(toolCallOfIndex).map(index => { + const tool = toolCallOfIndex[index] + return { name: tool.name, id: tool.id, params: tool.params } + }) + }); }) // when error/fail - this catches errors of both .create() and .then(for await) .catch(error => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts similarity index 88% rename from src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts rename to src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 2ae792fb..8bde8459 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/processMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -5,7 +5,14 @@ import { developerInfoOfModelName, developerInfoOfProviderName, ProviderName } f import { deepClone } from '../../../../../base/common/objects.js'; - +export const parseObject = (args: unknown) => { + if (typeof args === 'object') + return args + if (typeof args === 'string') + try { return JSON.parse(args) } + catch (e) { return { args } } + return {} +} // no matter whether the model supports a system message or not (or what format it supports), add it in some way // also take into account tools if the model doesn't support tool use @@ -118,7 +125,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: } | { type: 'tool_use'; name: string; - input: string; + input: Record; id: string; })[] } | { @@ -127,7 +134,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: type: 'text'; text: string; } | { - type: 'tool_response'; + type: 'tool_result'; tool_use_id: string; content: string; })[] @@ -141,17 +148,22 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: if (currMsg.role !== 'tool') continue const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined - const nextMsg = 0 <= i + 1 && i + 1 <= newMessagesTools.length ? newMessagesTools[i + 1] : undefined if (prevMsg?.role === 'assistant') { - if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: typeof prevMsg.content }] - prevMsg.content.push({ type: 'tool_use', name: currMsg.name, input: currMsg.params, id: currMsg.id }) + if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }] + prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) }) } - if (nextMsg?.role === 'user') { - if (typeof nextMsg.content === 'string') nextMsg.content = [{ type: 'text', text: typeof nextMsg.content }] - nextMsg.content.push({ type: 'tool_response', tool_use_id: currMsg.id, content: currMsg.content }) + + // turn each tool into a user message with tool results at the end + newMessagesTools[i] = { + role: 'user', + content: [ + ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const, + ...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [], + ] } } + finalMessages = newMessagesTools } @@ -212,7 +224,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: id: currMsg.id, function: { name: currMsg.name, - arguments: currMsg.params + arguments: JSON.stringify(currMsg.params) } }] } @@ -236,7 +248,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: console.log('SYSMG', separateSystemMessage) - console.log('FINAL MESSAGES', finalMessages) + console.log('FINAL MESSAGES', JSON.stringify(finalMessages, null, 2)) return { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index e568d3b5..9d179fd8 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -62,10 +62,10 @@ export const sendLLMMessage = ({ _fullTextSoFar = fullText } - const onFinalMessage: OnFinalMessage = ({ fullText, tools }) => { + const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls: tools }) => { if (_didAbort) return captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) - onFinalMessage_({ fullText, tools }) + onFinalMessage_({ fullText, toolCalls: tools }) } const onError: OnError = ({ message: error, fullError }) => { @@ -103,11 +103,11 @@ export const sendLLMMessage = ({ case 'ollama': case 'groq': case 'gemini': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', toolCalls: [] }) else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); break; case 'anthropic': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', tools: [] }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', toolCalls: [] }) else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); break; default: diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts index 9db9a68f..333f3919 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts @@ -100,7 +100,7 @@ export class LLMMessageChannel implements IServerChannel { const mainThreadParams: SendLLMMessageParams = { ...params, onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); }, - onFinalMessage: ({ fullText, tools }) => { this._onFinalMessage_llm.fire({ requestId, fullText, tools }); }, + onFinalMessage: ({ fullText, toolCalls: tools }) => { this._onFinalMessage_llm.fire({ requestId, fullText, toolCalls: tools }); }, onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); }, abortRef: this._abortRefOfRequestId_llm[requestId], } From 628adedaec3f76322539604dd7b79cb5611e4ed8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 16 Feb 2025 20:51:42 -0800 Subject: [PATCH 33/92] toolcalls --- .../contrib/void/browser/chatThreadService.ts | 10 +++++----- .../contrib/void/electron-main/llmMessage/anthropic.ts | 4 ++-- .../void/electron-main/llmMessage/sendLLMMessage.ts | 4 ++-- .../contrib/void/electron-main/llmMessageChannel.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 03286d38..98900354 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -326,15 +326,15 @@ class ChatThreadService extends Disposable implements IChatThreadService { onText: ({ fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) }, - onFinalMessage: async ({ fullText, toolCalls: tools }) => { - console.log('FINAL MESSAGE', fullText, tools) + onFinalMessage: async ({ fullText, toolCalls }) => { + console.log('FINAL MESSAGE', fullText, toolCalls) - if ((tools?.length ?? 0) === 0) { + if ((toolCalls?.length ?? 0) === 0) { this._finishStreamingTextMessage(threadId, fullText) } else { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText, tool_calls: tools }) - for (const tool of tools ?? []) { + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText, tool_calls: toolCalls }) + for (const tool of toolCalls ?? []) { if (!(tool.name in this._toolsService.toolFns)) { this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.params, id: tool.id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index fec9dc07..8308bb44 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -86,9 +86,9 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: stream.on('finalMessage', (response) => { // stringify the response's content const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null).filter(c => !!c) + const toolCalls = response.content.map(c => c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null).filter(c => !!c) - onFinalMessage({ fullText: content, toolCalls: tools }) + onFinalMessage({ fullText: content, toolCalls }) }) stream.on('error', (error) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 9d179fd8..980cf5b9 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -62,10 +62,10 @@ export const sendLLMMessage = ({ _fullTextSoFar = fullText } - const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls: tools }) => { + const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls }) => { if (_didAbort) return captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) - onFinalMessage_({ fullText, toolCalls: tools }) + onFinalMessage_({ fullText, toolCalls }) } const onError: OnError = ({ message: error, fullError }) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts index 333f3919..b00ade9c 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts @@ -100,7 +100,7 @@ export class LLMMessageChannel implements IServerChannel { const mainThreadParams: SendLLMMessageParams = { ...params, onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); }, - onFinalMessage: ({ fullText, toolCalls: tools }) => { this._onFinalMessage_llm.fire({ requestId, fullText, toolCalls: tools }); }, + onFinalMessage: ({ fullText, toolCalls }) => { this._onFinalMessage_llm.fire({ requestId, fullText, toolCalls }); }, onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); }, abortRef: this._abortRefOfRequestId_llm[requestId], } From a78a6169f8a7eccedad1759d7146a993e7b4c381 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 00:07:11 -0800 Subject: [PATCH 34/92] tools + misc fixes --- .../contrib/void/browser/chatThreadService.ts | 43 ++++++++++++------- .../void/browser/helpers/detectLanguage.ts | 4 ++ .../contrib/void/browser/helpers/readFile.ts | 5 +++ .../void/browser/helpers/systemInfo.ts | 17 ++++++++ .../contrib/void/browser/prompt/prompts.ts | 10 ++++- .../react/src/void-settings-tsx/Settings.tsx | 3 +- .../contrib/void/common/llmMessageTypes.ts | 6 +-- .../contrib/void/common/toolsService.ts | 2 +- 8 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 98900354..98aad294 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -14,7 +14,7 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; -import { InternalToolInfo, IToolsService, ToolName, voidTools } from '../common/toolsService.js'; +import { InternalToolInfo, IToolsService, ToolFns, ToolName, voidTools } from '../common/toolsService.js'; import { toLLMChatMessage } from '../common/llmMessageTypes.js'; // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) @@ -57,11 +57,6 @@ export type ChatMessage = staging: StagingInfo | null } | { role: 'assistant'; - tool_calls?: { - name: string, - id: string, - params: string - }[]; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored } | { @@ -327,25 +322,43 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { messageSoFar: fullText }) }, onFinalMessage: async ({ fullText, toolCalls }) => { + toolCalls = toolCalls?.filter(tool => tool.name in this._toolsService.toolFns) + console.log('FINAL MESSAGE', fullText, toolCalls) if ((toolCalls?.length ?? 0) === 0) { this._finishStreamingTextMessage(threadId, fullText) } else { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText, tool_calls: toolCalls }) + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText }) + this._setStreamState(threadId, { messageSoFar: undefined }) // clear streaming message for (const tool of toolCalls ?? []) { - if (!(tool.name in this._toolsService.toolFns)) { - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.params, id: tool.id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) + const toolName = tool.name as ToolName + + // 1. + let toolResult: Awaited> + try { + toolResult = await this._toolsService.toolFns[toolName](tool.params) + } catch (e) { + this._setStreamState(threadId, { error: e }) + shouldContinue = false + break } - else { - const toolName = tool.name as ToolName - const toolResult = await this._toolsService.toolFns[toolName](tool.params) - const string = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.params, id: tool.id, content: string, displayContent: string, }) - shouldContinue = true + + // 2. + let toolResultStr: string + try { + toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here + } catch (e) { + this._setStreamState(threadId, { error: e }) + shouldContinue = false + break } + + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.params, id: tool.id, content: toolResultStr, displayContent: toolResultStr, }) + shouldContinue = true } + } res_() }, diff --git a/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts b/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts index b4b9d513..9bc3c9bd 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts @@ -1,3 +1,7 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ // eg "bash" -> "shell" export const nameToVscodeLanguage: { [key: string]: string } = { diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts index 39cd310d..4b9e05da 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -1,3 +1,8 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + import { URI } from '../../../../../base/common/uri' import { EndOfLinePreference } from '../../../../../editor/common/model' import { IModelService } from '../../../../../editor/common/services/model.js' diff --git a/src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts b/src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts new file mode 100644 index 00000000..ab1dabad --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts @@ -0,0 +1,17 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js'; + +// import { OS, OperatingSystem } from '../../../../../base/common/platform.js'; +// alternatively could use ^ and OS === OperatingSystem.Windows ? ... + + + +export const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null + +export const arch = process.arch +export const osplatform = process.platform; + diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 415a0c87..105e1e93 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -9,6 +9,7 @@ import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; import { VSReadFile } from '../helpers/readFile.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; +import { os, arch, osplatform } from '../helpers/systemInfo.js'; // this is just for ease of readability @@ -19,17 +20,22 @@ You are a coding assistant. You are given a list of instructions to follow \`INS Please respond to the user's query. +The user has the following system information: + - ${os} ${arch} ${osplatform} + In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes. For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer. - Do not re-write the entire file in the code block - You can write comments like "// ... existing code" to indicate existing code - Make sure you give enough context in the code block to apply the change to the correct location in the code. -You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. - Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. Do not tell the user anything about the examples below. +If you are not given tools, you're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. +If you are given tools, you are allowed to use them without asking for permission. You do not have to use them if you don't want to. +If you are given tools, NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. + ## EXAMPLE 1 FILES math.ts diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index cbf8607c..0cfdef04 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -17,6 +17,7 @@ import { env } from '../../../../../../../base/common/process.js' import { ModelDropdown } from './ModelDropdown.js' import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js' import { WarningBox } from './WarningBox.js' +import { os } from '../../../helpers/systemInfo.js' const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => { @@ -505,7 +506,7 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): Transfe throw new Error(`os '${os}' not recognized`) } -const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null + let transferTheseFiles: TransferFilesInfo = [] let transferError: string | null = null diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 79960826..75cf2739 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -52,12 +52,12 @@ export type AbortRef = { current: (() => void) | null } export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { if (c.role === 'system' || c.role === 'user') { - return { role: c.role, content: c.content ?? '(empty)' } + return { role: c.role, content: c.content || '(empty message)' } } else if (c.role === 'assistant') - return { role: c.role, content: c.content ?? '(empty model output)' } + return { role: c.role, content: c.content || '(empty message)' } else if (c.role === 'tool') - return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content ?? '(empty output)' } + return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content || '(empty output)' } else { throw 1 } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 0c7c7f82..2dde2219 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -111,7 +111,7 @@ async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): const validateURI = (uriStr: unknown) => { - if (typeof uriStr !== 'string') throw new Error('(uri was not a string)') + if (typeof uriStr !== 'string') throw new Error('(provided uri must be a string)') const uri = URI.file(uriStr) return uri } From fecfbd924ad1c22f0efb71e0e523a5e409a8b20a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 00:07:33 -0800 Subject: [PATCH 35/92] error --- .../workbench/contrib/void/browser/chatThreadService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 98aad294..ea435ed4 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -339,8 +339,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { let toolResult: Awaited> try { toolResult = await this._toolsService.toolFns[toolName](tool.params) - } catch (e) { - this._setStreamState(threadId, { error: e }) + } catch (error) { + this._setStreamState(threadId, { error }) shouldContinue = false break } @@ -349,8 +349,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { let toolResultStr: string try { toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here - } catch (e) { - this._setStreamState(threadId, { error: e }) + } catch (error) { + this._setStreamState(threadId, { error }) shouldContinue = false break } From 5608aca1689001bf658bc1a434a5eddc5e4936bb Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 00:17:14 -0800 Subject: [PATCH 36/92] process --- src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts | 3 --- src/vs/workbench/contrib/void/browser/prompt/prompts.ts | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts b/src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts index ab1dabad..85b909b2 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/systemInfo.ts @@ -12,6 +12,3 @@ import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/plat export const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null -export const arch = process.arch -export const osplatform = process.platform; - diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 105e1e93..04a0f9f1 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -9,7 +9,7 @@ import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; import { VSReadFile } from '../helpers/readFile.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; -import { os, arch, osplatform } from '../helpers/systemInfo.js'; +import { os } from '../helpers/systemInfo.js'; // this is just for ease of readability @@ -21,7 +21,7 @@ You are a coding assistant. You are given a list of instructions to follow \`INS Please respond to the user's query. The user has the following system information: - - ${os} ${arch} ${osplatform} + - ${os} In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes. For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer. From b74b031906bb0f7fbc6c39de36da0056f37c2ab2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 00:18:55 -0800 Subject: [PATCH 37/92] + --- src/vs/workbench/contrib/void/browser/prompt/prompts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 04a0f9f1..6f888916 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -32,8 +32,8 @@ For example, if the user asks you to "make this file look nicer", make sure your Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. Do not tell the user anything about the examples below. -If you are not given tools, you're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. -If you are given tools, you are allowed to use them without asking for permission. You do not have to use them if you don't want to. +You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. +If you are given tools, you are allowed to use them without asking for permission. You do not have to use them if you don't want to, but you may use them to gather context, etc. If you are given tools, NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. ## EXAMPLE 1 From 491312218fbb5539b8619e39ae8d0e8dc3a8f848 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 00:19:39 -0800 Subject: [PATCH 38/92] + --- src/vs/workbench/contrib/void/browser/prompt/prompts.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 6f888916..b0eeacaa 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -29,13 +29,14 @@ For example, if the user asks you to "make this file look nicer", make sure your - You can write comments like "// ... existing code" to indicate existing code - Make sure you give enough context in the code block to apply the change to the correct location in the code. -Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. -Do not tell the user anything about the examples below. - You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. If you are given tools, you are allowed to use them without asking for permission. You do not have to use them if you don't want to, but you may use them to gather context, etc. If you are given tools, NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. +Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. +Do not tell the user anything about the examples below. + + ## EXAMPLE 1 FILES math.ts From 8a8ed1ac56848fc77d192ffa409430aedad89ab5 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 00:20:01 -0800 Subject: [PATCH 39/92] + --- src/vs/workbench/contrib/void/browser/prompt/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index b0eeacaa..949e1cba 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -34,7 +34,7 @@ If you are given tools, you are allowed to use them without asking for permissio If you are given tools, NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. -Do not tell the user anything about the examples below. +Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below. ## EXAMPLE 1 From 89d08071fcbc45af5477e7a2bf160e9b0b80b3eb Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 17 Feb 2025 00:23:48 -0800 Subject: [PATCH 40/92] scrollbar fix --- .../react/src/util/useScrollbarStyles.tsx | 135 +++++++++--------- 1 file changed, 66 insertions(+), 69 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx index 94df3aac..1ba69c24 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx @@ -19,90 +19,87 @@ export const useScrollbarStyles = (containerRef: React.MutableRefObject { - if ((element as any).__scrollbarCleanup) { - (element as any).__scrollbarCleanup(); - } + element.classList.add('void-scrollable-element'); }); - // Apply styles and listeners to each scroll element + // Only initialize fade effects for elements that haven't been initialized yet scrollElements.forEach(element => { - // Add the scrollable class directly to the overflow element - element.classList.add('void-scrollable-element'); + if (!(element as any).__scrollbarCleanup) { + let fadeTimeout: NodeJS.Timeout | null = null; + let fadeInterval: NodeJS.Timeout | null = null; - let fadeTimeout: NodeJS.Timeout | null = null; - let fadeInterval: NodeJS.Timeout | null = null; + const fadeIn = () => { + if (fadeInterval) clearInterval(fadeInterval); - const fadeIn = () => { - if (fadeInterval) clearInterval(fadeInterval); + let step = 0; + fadeInterval = setInterval(() => { + if (step <= 10) { + element.classList.remove(`show-scrollbar-${step - 1}`); + element.classList.add(`show-scrollbar-${step}`); + step++; + } else { + clearInterval(fadeInterval!); + } + }, 10); + }; - let step = 0; - fadeInterval = setInterval(() => { - if (step <= 10) { - element.classList.remove(`show-scrollbar-${step - 1}`); - element.classList.add(`show-scrollbar-${step}`); - step++; - } else { - clearInterval(fadeInterval!); + const fadeOut = () => { + if (fadeInterval) clearInterval(fadeInterval); + + let step = 10; + fadeInterval = setInterval(() => { + if (step >= 0) { + element.classList.remove(`show-scrollbar-${step + 1}`); + element.classList.add(`show-scrollbar-${step}`); + step--; + } else { + clearInterval(fadeInterval!); + } + }, 60); + }; + + const onMouseEnter = () => { + if (fadeTimeout) clearTimeout(fadeTimeout); + if (fadeInterval) clearInterval(fadeInterval); + fadeIn(); + }; + + const onMouseLeave = () => { + if (fadeTimeout) clearTimeout(fadeTimeout); + fadeTimeout = setTimeout(() => { + fadeOut(); + }, 10); + }; + + element.addEventListener('mouseenter', onMouseEnter); + element.addEventListener('mouseleave', onMouseLeave); + + // Store cleanup function + const cleanup = () => { + element.removeEventListener('mouseenter', onMouseEnter); + element.removeEventListener('mouseleave', onMouseLeave); + if (fadeTimeout) clearTimeout(fadeTimeout); + if (fadeInterval) clearInterval(fadeInterval); + element.classList.remove('void-scrollable-element'); + // Remove any remaining show-scrollbar classes + for (let i = 0; i <= 10; i++) { + element.classList.remove(`show-scrollbar-${i}`); } - }, 10); - }; + }; - const fadeOut = () => { - if (fadeInterval) clearInterval(fadeInterval); - - let step = 10; - fadeInterval = setInterval(() => { - if (step >= 0) { - element.classList.remove(`show-scrollbar-${step + 1}`); - element.classList.add(`show-scrollbar-${step}`); - step--; - } else { - clearInterval(fadeInterval!); - } - }, 60); - }; - - const onMouseEnter = () => { - if (fadeTimeout) clearTimeout(fadeTimeout); - if (fadeInterval) clearInterval(fadeInterval); - fadeIn(); - }; - - const onMouseLeave = () => { - if (fadeTimeout) clearTimeout(fadeTimeout); - fadeTimeout = setTimeout(() => { - fadeOut(); - }, 10); - }; - - element.addEventListener('mouseenter', onMouseEnter); - element.addEventListener('mouseleave', onMouseLeave); - - // Store cleanup function - const cleanup = () => { - element.removeEventListener('mouseenter', onMouseEnter); - element.removeEventListener('mouseleave', onMouseLeave); - if (fadeTimeout) clearTimeout(fadeTimeout); - if (fadeInterval) clearInterval(fadeInterval); - element.classList.remove('void-scrollable-element'); - // Remove any remaining show-scrollbar classes - for (let i = 0; i <= 10; i++) { - element.classList.remove(`show-scrollbar-${i}`); - } - }; - - // Store the cleanup function on the element for later use - (element as any).__scrollbarCleanup = cleanup; + // Store the cleanup function on the element for later use + (element as any).__scrollbarCleanup = cleanup; + } }); }; // Initialize for the first time initializeScrollbarStyles(); - // Set up mutation observer - const observer = new MutationObserver((mutations) => { + // Set up mutation observer to do the same + const observer = new MutationObserver(() => { initializeScrollbarStyles(); }); From 366dbf0b52470a6d734524745d498e8235b66788 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 01:06:21 -0800 Subject: [PATCH 41/92] add xAI and update system/tool information and system prompt --- .../contrib/void/browser/chatThreadService.ts | 4 +- .../contrib/void/browser/prompt/prompts.ts | 3 +- .../react/src/markdown/ChatMarkdownRender.tsx | 15 +- .../void/common/voidSettingsService.ts | 3 + .../contrib/void/common/voidSettingsTypes.ts | 162 +++++++++++------- .../void/electron-main/llmMessage/openai.ts | 8 +- .../llmMessage/preprocessLLMMessages.ts | 4 +- .../llmMessage/sendLLMMessage.ts | 1 + 8 files changed, 123 insertions(+), 77 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index ea435ed4..dab3e62e 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -16,6 +16,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; import { InternalToolInfo, IToolsService, ToolFns, ToolName, voidTools } from '../common/toolsService.js'; import { toLLMChatMessage } from '../common/llmMessageTypes.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { @@ -161,6 +162,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IModelService private readonly _modelService: IModelService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IToolsService private readonly _toolsService: IToolsService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, ) { super() @@ -312,7 +314,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { useProviderFor: 'Ctrl+L', logging: { loggingName: `Agent` }, messages: [ - { role: 'system', content: chat_systemMessage }, + { role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)) }, ...this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))), ], diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 949e1cba..88e9144d 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -15,13 +15,14 @@ import { os } from '../helpers/systemInfo.js'; // this is just for ease of readability export const tripleTick = ['```', '```'] -export const chat_systemMessage = `\ +export const chat_systemMessage = (workspaces: string[]) => `\ You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`. Please respond to the user's query. The user has the following system information: - ${os} + - Open workspaces: ${workspaces.join(', ')} In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes. For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer. diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index ded3eaff..f8184db1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -97,7 +97,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c } -const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation: chatLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { +const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { // deal with built-in tokens first (assume marked token) @@ -111,16 +111,17 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati if (t.type === "code") { const isCodeblockClosed = t.raw?.startsWith('```') && t.raw?.endsWith('```'); - const applyBoxId = getApplyBoxId({ - threadId: chatLocation!.threadId, - messageIdx: chatLocation!.messageIdx, + // this should never be + const applyBoxId = chatMessageLocation ? getApplyBoxId({ + threadId: chatMessageLocation.threadId, + messageIdx: chatMessageLocation.messageIdx, tokenIdx: tokenIdx, - }) + }) : null return } + buttonsOnHover={applyBoxId && } /> } @@ -195,7 +196,7 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati )} - + ))} diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index eac87692..8fd4aa79 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -175,6 +175,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS) ...{ mistral: defaultSettingsOfProvider.mistral }, + // A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS) + ...{ mistral: defaultSettingsOfProvider.xAI }, + ...readS.settingsOfProvider, // A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 0bbcfcde..abef16d8 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -11,17 +11,15 @@ import { VoidSettingsState } from './voidSettingsService.js' // developer info used in sendLLMMessage export type DeveloperInfoAtModel = { // USED: - - // TODO!!! think tokens - deepseek - - // TODO!!!! - // UNUSED (coming soon): - recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized + supportsSystemMessage: 'developer' | boolean, // if null, we will just do a string of system message. this is independent from separateSystemMessage, which takes priority and is passed directly in each provider's implementation. supportsTools: boolean, // we will just do a string of tool use if it doesn't support - supportsSystemMessageRole: 'developer' | 'system' | false, // if null, we will just do a string of system message. this is independent from separateSystemMessage, which takes priority and is passed directly in each provider's implementation. - supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> - supportsStreaming: boolean, // (o1 does NOT) we will just dump the final result if doesn't support it - maxTokens: number, // required + + // UNUSED (coming soon): + // TODO!!! think tokens - deepseek + _recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized + _supportsStreaming: boolean, // we will just dump the final result if doesn't support it + _supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> + _maxTokens: number, // required } export type DeveloperInfoAtProvider = { @@ -49,6 +47,7 @@ export const recognizedModels = [ 'Anthropic Claude', 'Llama 3.x', 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model + 'xAI Grok', // 'xAI Grok', // 'Google Gemini, Gemma', // 'Microsoft Phi4', @@ -59,7 +58,7 @@ export const recognizedModels = [ 'Mistral Codestral', // thinking - 'OpenAI o1, o3', + 'OpenAI o1', 'Deepseek R1', // general @@ -85,11 +84,13 @@ export function recognizedModelOfModelName(modelName: string): RecognizedModelNa if (lower.includes('mistral')) return 'Mistral Codestral'; if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 - return 'OpenAI o1, o3'; + return 'OpenAI o1'; if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return 'Deepseek R1'; if (lower.includes('deepseek')) return 'Deepseek Chat' + if (lower.includes('grok')) + return 'xAI Grok' return ''; } @@ -98,18 +99,14 @@ export function recognizedModelOfModelName(modelName: string): RecognizedModelNa const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { 'anthropic': { overrideSettingsForAllModels: { - supportsSystemMessageRole: 'system', + supportsSystemMessage: true, supportsTools: true, - supportsAutocompleteFIM: false, - supportsStreaming: true, + _supportsAutocompleteFIM: false, + _supportsStreaming: true, } }, 'deepseek': { overrideSettingsForAllModels: { - supportsSystemMessageRole: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: true, } }, 'ollama': { @@ -126,6 +123,8 @@ const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAt }, 'groq': { }, + 'xAI': { + }, } export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { return developerInfoAtProvider[providerName] ?? {} @@ -135,83 +134,93 @@ export const developerInfoOfProviderName = (providerName: ProviderName): Partial // providerName is optional, but gives some extra fallbacks if provided -const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { +const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { 'OpenAI 4o': { - supportsSystemMessageRole: 'system', + supportsSystemMessage: true, supportsTools: true, - supportsAutocompleteFIM: false, - supportsStreaming: true, - maxTokens: 4096, + _supportsAutocompleteFIM: false, + _supportsStreaming: true, + _maxTokens: 4096, }, 'Anthropic Claude': { - supportsSystemMessageRole: 'system', + supportsSystemMessage: true, supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, + _supportsAutocompleteFIM: false, + _supportsStreaming: false, + _maxTokens: 4096, }, 'Llama 3.x': { - supportsSystemMessageRole: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, + supportsSystemMessage: true, + supportsTools: true, + _supportsAutocompleteFIM: false, + _supportsStreaming: false, + _maxTokens: 4096, + }, + + 'xAI Grok': { + supportsSystemMessage: true, + supportsTools: true, + _supportsAutocompleteFIM: false, + _supportsStreaming: true, + _maxTokens: 4096, + }, 'Deepseek Chat': { - supportsSystemMessageRole: false, + supportsSystemMessage: true, supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, + _supportsAutocompleteFIM: false, + _supportsStreaming: false, + _maxTokens: 4096, }, 'Alibaba Qwen2.5 Coder Instruct': { - supportsSystemMessageRole: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, + supportsSystemMessage: true, + supportsTools: true, + _supportsAutocompleteFIM: false, + _supportsStreaming: false, + _maxTokens: 4096, }, 'Mistral Codestral': { - supportsSystemMessageRole: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, + supportsSystemMessage: true, + supportsTools: true, + _supportsAutocompleteFIM: false, + _supportsStreaming: false, + _maxTokens: 4096, }, - 'OpenAI o1, o3': { - supportsSystemMessageRole: false, + 'OpenAI o1': { + supportsSystemMessage: 'developer', supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, + _supportsAutocompleteFIM: false, + _supportsStreaming: true, + _maxTokens: 4096, }, 'Deepseek R1': { - supportsSystemMessageRole: false, + supportsSystemMessage: false, supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, + _supportsAutocompleteFIM: false, + _supportsStreaming: false, + _maxTokens: 4096, }, + '': { - supportsSystemMessageRole: false, + supportsSystemMessage: false, supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, + _supportsAutocompleteFIM: false, + _supportsStreaming: false, + _maxTokens: 4096, }, } export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { const recognizedModelName = recognizedModelOfModelName(modelName) return { - recognizedModelName: recognizedModelName, + _recognizedModelName: recognizedModelName, ...developerInfoOfRecognizedModelName[recognizedModelName], ...overrides } @@ -323,6 +332,10 @@ export const defaultMistralModels = modelInfoOfDefaultModelNames([ "mistral-small-latest", ]) +export const defaultXAIModels = modelInfoOfDefaultModelNames([ + 'grok-2-latest', + 'grok-3-latest', +]) // export const parseMaxTokensStr = (maxTokensStr: string) => { // // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN // const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) @@ -378,6 +391,9 @@ export const defaultProviderSettings = { }, mistral: { apiKey: '' + }, + xAI: { + apiKey: '' } } as const @@ -446,7 +462,6 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn else if (providerName === 'ollama') { return { title: 'Ollama', - } } else if (providerName === 'openAICompatible') { @@ -469,6 +484,12 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn title: 'Mistral API', } } + else if (providerName === 'xAI') { + return { + title: 'xAI API', + } + } + throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) } @@ -493,7 +514,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'groq' ? 'gsk_key...' : providerName === 'mistral' ? 'key...' : providerName === 'openAICompatible' ? 'sk-key...' : - '', + providerName === 'xAI' ? 'xai-key...' : + '', subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : @@ -502,8 +524,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' : - providerName === 'openAICompatible' ? undefined : - '', + providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : + providerName === 'openAICompatible' ? undefined : + '', } } else if (settingName === 'endpoint') { @@ -574,6 +597,9 @@ export const voidInitModelOptions = { }, mistral: { models: defaultMistralModels, + }, + xAI: { + models: defaultXAIModels, } } @@ -610,6 +636,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...voidInitModelOptions.mistral, _didFillInProviderSettings: undefined, }, + xAI: { + ...defaultCustomSettings, + ...defaultProviderSettings.xAI, + ...voidInitModelOptions.xAI, + _didFillInProviderSettings: undefined, + }, groq: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.groq, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 49dd0bfd..4b9ab724 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -91,9 +91,15 @@ const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, }) } + else if (providerName === 'xAI') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ + baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, + }) + } else { console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`) - throw new Error(`providerName was invalid: ${providerName}`) + throw new Error(`Void providerName was invalid: ${providerName}`) } } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 8bde8459..eba90468 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -21,7 +21,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), })) const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const { supportsSystemMessageRole: supportsSystemMessage, supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) + const { supportsSystemMessage, supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) // 1. SYSTEM MESSAGE // find system messages and concatenate them @@ -52,7 +52,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: if (separateSystemMessage) separateSystemMessageStr = systemMessageStr else { - newMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message + newMessages.unshift({ role: supportsSystemMessage === 'developer' ? 'developer' : 'system', content: systemMessageStr }) // add new first message } } // if does not support system message diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 980cf5b9..7d83cff2 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -103,6 +103,7 @@ export const sendLLMMessage = ({ case 'ollama': case 'groq': case 'gemini': + case 'xAI': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', toolCalls: [] }) else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); break; From 137e0068a3679b4705ecb20c73cc37b6eb40ff0b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 01:50:28 -0800 Subject: [PATCH 42/92] add vllm --- .../contrib/void/browser/editCodeService.ts | 6 ---- .../void/common/refreshModelService.ts | 10 ++++-- .../void/common/voidSettingsService.ts | 6 +++- .../contrib/void/common/voidSettingsTypes.ts | 35 +++++++++++++++---- .../void/electron-main/llmMessage/openai.ts | 6 ++++ .../llmMessage/sendLLMMessage.ts | 3 +- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 0138cfc1..8468a241 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1177,19 +1177,13 @@ class EditCodeService extends Disposable implements IEditCodeService { private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { - - console.log('SEARCHREPLACE') const uri_ = this._getActiveEditorURI() if (!uri_) return const uri = uri_ - console.log('/* AAAA */') // generate search/replace block text const fileContents = await VSReadFile(this._modelService, uri) if (fileContents === null) return - console.log('/* BBB*/') - - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index ff61e8a8..1c95a4ad 100644 --- a/src/vs/workbench/contrib/void/common/refreshModelService.ts +++ b/src/vs/workbench/contrib/void/common/refreshModelService.ts @@ -45,6 +45,7 @@ export type RefreshModelStateOfProvider = Record { this._clearProviderTimeout(providerName) @@ -158,8 +160,9 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ } } const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList - : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList - : () => { } + : providerName === 'vLLM' ? this.llmMessageService.openAICompatibleList + : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList + : () => { } listFn({ onSuccess: ({ models }) => { @@ -169,6 +172,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ providerName, models.map(model => { if (providerName === 'ollama') return (model as OllamaModelResponse).name; + else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id; else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id; else throw new Error('refreshMode fn: unknown provider', providerName); }), diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 8fd4aa79..7a35c678 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -176,7 +176,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { ...{ mistral: defaultSettingsOfProvider.mistral }, // A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS) - ...{ mistral: defaultSettingsOfProvider.xAI }, + ...{ xAI: defaultSettingsOfProvider.xAI }, + + // A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS) + ...{ vLLM: defaultSettingsOfProvider.vLLM }, + ...readS.settingsOfProvider, diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index abef16d8..14371a22 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -125,6 +125,8 @@ const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAt }, 'xAI': { }, + 'vLLM': { + }, } export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { return developerInfoAtProvider[providerName] ?? {} @@ -376,6 +378,9 @@ export const defaultProviderSettings = { ollama: { endpoint: 'http://127.0.0.1:11434', }, + vLLM: { + endpoint: 'http://localhost:8000', + }, openRouter: { apiKey: '', }, @@ -394,13 +399,13 @@ export const defaultProviderSettings = { }, xAI: { apiKey: '' - } + }, } as const export type ProviderName = keyof typeof defaultProviderSettings export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[] -export const localProviderNames = ['ollama'] satisfies ProviderName[] // all local names +export const localProviderNames = ['ollama', 'vLLM'] satisfies ProviderName[] // all local names export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names type CustomSettingName = UnionOfKeys @@ -464,6 +469,11 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn title: 'Ollama', } } + else if (providerName === 'vLLM') { + return { + title: 'vLLM', + } + } else if (providerName === 'openAICompatible') { return { title: 'OpenAI-Compatible', @@ -532,12 +542,14 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName else if (settingName === 'endpoint') { return { title: providerName === 'ollama' ? 'Endpoint' : - providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions) - : '(never)', + providerName === 'vLLM' ? 'Endpoint' : + providerName === 'openAICompatible' ? 'baseURL' :// (do not include /chat/completions) + '(never)', placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint - : providerName === 'openAICompatible' ? 'https://my-website.com/v1' - : '(never)', + : providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint + : providerName === 'openAICompatible' ? 'https://my-website.com/v1' + : '(never)', subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' : undefined, @@ -583,6 +595,9 @@ export const voidInitModelOptions = { ollama: { models: [], }, + vLLM: { + models: [], + }, openRouter: { models: [], // any string }, @@ -601,7 +616,7 @@ export const voidInitModelOptions = { xAI: { models: defaultXAIModels, } -} +} satisfies Record // used when waiting and for a type reference @@ -666,6 +681,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...voidInitModelOptions.ollama, _didFillInProviderSettings: undefined, }, + vLLM: { // aggregator + ...defaultCustomSettings, + ...defaultProviderSettings.vLLM, + ...voidInitModelOptions.vLLM, + _didFillInProviderSettings: undefined, + }, } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 4b9ab724..80db8d73 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -51,6 +51,12 @@ const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, }) } + else if (providerName === 'vLLM') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ + baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, + }) + } else if (providerName === 'openRouter') { const thisConfig = settingsOfProvider[providerName] return new OpenAI({ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 7d83cff2..8e29bff4 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -101,10 +101,11 @@ export const sendLLMMessage = ({ case 'openAICompatible': case 'mistral': case 'ollama': + case 'vLLM': case 'groq': case 'gemini': case 'xAI': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', toolCalls: [] }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI API FIM', toolCalls: [] }) else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); break; case 'anthropic': From 0fd10f404e3f4f316b259379819bc2df15ddc75d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 14:12:52 -0800 Subject: [PATCH 43/92] tool results in ChatMessage --- .../contrib/void/browser/chatThreadService.ts | 27 ++++++++++--------- .../contrib/void/browser/prompt/prompts.ts | 6 ++--- .../contrib/void/common/toolsService.ts | 12 ++++----- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index dab3e62e..22c99c67 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -14,7 +14,7 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; -import { InternalToolInfo, IToolsService, ToolFns, ToolName, voidTools } from '../common/toolsService.js'; +import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js'; import { toLLMChatMessage } from '../common/llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -43,6 +43,15 @@ export type StagingInfo = { const defaultStaging: StagingInfo = { isBeingEdited: false, selections: [] } +type ToolMessage = { + role: 'tool'; + name: T; // internal use + params: string; // internal use + id: string; // apis require this tool use id + content: string; // result + result: ToolCallReturnType; // text message of result +} + // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. export type ChatMessage = @@ -60,14 +69,7 @@ export type ChatMessage = role: 'assistant'; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored - } | { - role: 'tool'; - name: string; // internal use - params: string; // internal use - id: string; // apis require this tool use id - content: string; // result - displayContent: string; // text message of result - } + } | ToolMessage // a 'thread' means a chat message history export type ChatThreads = { @@ -323,8 +325,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { onText: ({ fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) }, - onFinalMessage: async ({ fullText, toolCalls }) => { - toolCalls = toolCalls?.filter(tool => tool.name in this._toolsService.toolFns) + onFinalMessage: async ({ fullText, toolCalls: toolCalls_ }) => { + // make sure all tool names are valid so we can cast to ToolName below + const toolCalls = toolCalls_?.filter(tool => tool.name in this._toolsService.toolFns) console.log('FINAL MESSAGE', fullText, toolCalls) @@ -357,7 +360,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { break } - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.params, id: tool.id, content: toolResultStr, displayContent: toolResultStr, }) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResult, }) shouldContinue = true } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 88e9144d..2b840f83 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -18,7 +18,7 @@ export const tripleTick = ['```', '```'] export const chat_systemMessage = (workspaces: string[]) => `\ You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`. -Please respond to the user's query. +Please respond to the user's query. The user's query is never invalid. The user has the following system information: - ${os} @@ -26,8 +26,8 @@ The user has the following system information: In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes. For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer. - - Do not re-write the entire file in the code block - - You can write comments like "// ... existing code" to indicate existing code + - Do not re-write the entire file in the code block. + - You can write comments like "// ... existing code" to indicate existing code. - Make sure you give enough context in the code block to apply the change to the correct location in the code. You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 2dde2219..07733c86 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -81,14 +81,14 @@ export type ToolParamsObj = { [paramName in ToolParamNames - = T extends 'read_file' ? Promise - : T extends 'list_dir' ? Promise - : T extends 'pathname_search' ? Promise - : T extends 'search' ? Promise + = T extends 'read_file' ? string + : T extends 'list_dir' ? string + : T extends 'pathname_search' ? string | URI[] + : T extends 'search' ? string | URI[] : never -export type ToolFns = { [T in ToolName]: (p: string) => ToolCallReturnType } -export type ToolResultToString = { [T in ToolName]: (result: Awaited>) => string } +export type ToolFns = { [T in ToolName]: (p: string) => Promise> } +export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType) => string } async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise { From b92420012c01a69a698152d3b5d5db44a90e7f94 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 17 Feb 2025 15:18:11 -0800 Subject: [PATCH 44/92] minor --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 1 - 1 file changed, 1 deletion(-) 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 52944476..88b47355 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 @@ -703,7 +703,6 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM : role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre : role === 'assistant' ? `px-2 self-start w-full max-w-full` : '' } - ${role !== 'assistant' ? 'my-2' : ''} `} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} From d3547134e7552eff9b650cfb00f0e5d83d042004 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 15:22:24 -0800 Subject: [PATCH 45/92] get ready to add searchReplaceService --- .../contrib/void/browser/editCodeService.ts | 4 +- .../void/browser/searchReplaceService.ts | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/contrib/void/browser/searchReplaceService.ts diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 8468a241..989086a6 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1192,7 +1192,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const userMessageContent = searchReplace_userMessage({ originalCode: fileContents, applyStr: applyStr }) const messages: LLMChatMessage[] = [ { role: 'system', content: searchReplace_systemMessage }, - { role: 'user', content: userMessageContent } + { role: 'user', content: userMessageContent }, ] let streamRequestIdRef: { current: string | null } = { current: null } @@ -1232,8 +1232,10 @@ class EditCodeService extends Disposable implements IEditCodeService { } + // any time there's an error, add assistant's message, then user message saying the problem and to retry // TODO!!! turn this into a service and provide it + // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', useProviderFor: 'Apply', diff --git a/src/vs/workbench/contrib/void/browser/searchReplaceService.ts b/src/vs/workbench/contrib/void/browser/searchReplaceService.ts new file mode 100644 index 00000000..9bf958c4 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/searchReplaceService.ts @@ -0,0 +1,37 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILLMMessageService } from '../common/llmMessageService.js'; + + + +export interface ISearchReplaceService { + readonly _serviceBrand: undefined; +} + +export const ISearchReplaceService = createDecorator('SearchReplaceService'); +class SearchReplaceService extends Disposable implements ISearchReplaceService { + _serviceBrand: undefined; + + static readonly ID = 'SearchReplaceService'; + + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; + + constructor( + @ILLMMessageService private readonly llmMessageService: ILLMMessageService, + ) { + super() + // this.llmMessageService.sendLLMMessage({}) + + } + +} + +registerSingleton(ISearchReplaceService, SearchReplaceService, InstantiationType.Eager); From d3aa0bc3cc5a5da3cfc29d42ba5c33814c1112e2 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 17 Feb 2025 17:56:20 -0800 Subject: [PATCH 46/92] fix readfile --- .../contrib/void/browser/helpers/readFile.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts index f7752b84..7c03f036 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -20,7 +20,7 @@ export const VSReadFile = async (modelService: IModelService, fileService: IFile // read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.) export const _VSReadModel = async (modelService: IModelService, uri: URI): Promise => { - // attempt to read saved model (sometimes doesn't work if page is reloaded) + // attempt to read saved model (doesn't work if application was reloaded...) const model = modelService.getModel(uri) if (model) { return model.getValue(EndOfLinePreference.LF) @@ -38,7 +38,12 @@ export const _VSReadModel = async (modelService: IModelService, uri: URI): Promi } export const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => { - const res = await fileService.readFile(uri) - const str = res.value.toString() - return str + try { + const res = await fileService.readFile(uri) + const str = res.value.toString() + return str + } catch (e) { + return '' + } + } From 667769c987741d38b8c102348bce99fa68bf1a58 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 23:57:59 -0800 Subject: [PATCH 47/92] tool update; multiple SEARCH/REPLACE blocks --- .../contrib/void/browser/aiRegexService.ts | 2 +- .../contrib/void/browser/chatThreadService.ts | 18 +- .../contrib/void/browser/editCodeService.ts | 243 ++++++++++++------ .../contrib/void/browser/prompt/prompts.ts | 37 +-- ...ervice.ts => searchReplaceCacheService.ts} | 17 +- .../contrib/void/common/voidSettingsTypes.ts | 3 +- 6 files changed, 206 insertions(+), 114 deletions(-) rename src/vs/workbench/contrib/void/browser/{searchReplaceService.ts => searchReplaceCacheService.ts} (75%) diff --git a/src/vs/workbench/contrib/void/browser/aiRegexService.ts b/src/vs/workbench/contrib/void/browser/aiRegexService.ts index c0ae27fa..f38236a9 100644 --- a/src/vs/workbench/contrib/void/browser/aiRegexService.ts +++ b/src/vs/workbench/contrib/void/browser/aiRegexService.ts @@ -44,7 +44,7 @@ export const IVoidFastApplyService = createDecorator('voidFas class VoidFastApplyService extends Disposable implements IFastApplyService { _serviceBrand: undefined; - static readonly ID = 'voidFastApplyService'; + // static readonly ID = 'voidFastApplyService'; private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 22c99c67..244256a6 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -304,9 +304,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { // agent loop const agentLoop = async () => { - let shouldContinue = false - do { - shouldContinue = false + let shouldSendAnotherMessage = true + let nMessagesSent = 0 + + while (shouldSendAnotherMessage) { + shouldSendAnotherMessage = false + nMessagesSent += 1 let res_: () => void const awaitable = new Promise((res, rej) => { res_ = res }) @@ -329,8 +332,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { // make sure all tool names are valid so we can cast to ToolName below const toolCalls = toolCalls_?.filter(tool => tool.name in this._toolsService.toolFns) - console.log('FINAL MESSAGE', fullText, toolCalls) - if ((toolCalls?.length ?? 0) === 0) { this._finishStreamingTextMessage(threadId, fullText) } @@ -346,7 +347,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResult = await this._toolsService.toolFns[toolName](tool.params) } catch (error) { this._setStreamState(threadId, { error }) - shouldContinue = false + shouldSendAnotherMessage = false break } @@ -356,12 +357,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here } catch (error) { this._setStreamState(threadId, { error }) - shouldContinue = false + shouldSendAnotherMessage = false break } this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResult, }) - shouldContinue = true + shouldSendAnotherMessage = true } } @@ -377,7 +378,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { await awaitable } - while (shouldContinue); } agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 989086a6..b0a0d197 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1182,14 +1182,14 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = uri_ // generate search/replace block text - const fileContents = await VSReadFile(this._modelService, uri) - if (fileContents === null) return + const origFileContents = await VSReadFile(this._modelService, uri) + if (origFileContents === null) return // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - const userMessageContent = searchReplace_userMessage({ originalCode: fileContents, applyStr: applyStr }) + const userMessageContent = searchReplace_userMessage({ originalCode: origFileContents, applyStr: applyStr }) const messages: LLMChatMessage[] = [ { role: 'system', content: searchReplace_systemMessage }, { role: 'user', content: userMessageContent }, @@ -1197,6 +1197,7 @@ class EditCodeService extends Disposable implements IEditCodeService { let streamRequestIdRef: { current: string | null } = { current: null } const diffareaidOfBlockNum: number[] = [] + const diffAreaOriginalLines: [number, number][] = [] // TODO replace all these with whatever block we're on initially if already started let latestStreamLocationMutable: StreamLocationMutable | null = null @@ -1215,10 +1216,15 @@ class EditCodeService extends Disposable implements IEditCodeService { return [startLine, endLine] } - const { onFinishEdit } = this._addToHistory(uri) + let { onFinishEdit } = this._addToHistory(uri) + const revertAndContinueHistory = () => { + this._undoHistory(uri) + const { onFinishEdit: onFinishEdit_ } = this._addToHistory(uri) + onFinishEdit = onFinishEdit_ + } - const onDone = (hadError: boolean) => { + const onDone = (errorMessage: false | string) => { for (const blockNum in diffareaidOfBlockNum) { const diffareaid = diffareaidOfBlockNum[blockNum] const diffZone = this.diffAreaOfId[diffareaid] @@ -1227,106 +1233,175 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) } this._refreshStylesAndDiffsInURI(uri) - if (hadError) this._undoHistory(uri) + if (errorMessage) { + this._notificationService.info(`Void had an error when running Apply: ${errorMessage}.\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) this error.`) + this._metricsService.capture('Error - Apply', { errorMessage }) + this._undoHistory(uri) + } onFinishEdit() } - // any time there's an error, add assistant's message, then user message saying the problem and to retry + const onNewBlockStart = (blockNum: number, block: ExtractedSearchReplaceBlock): { errorStartingBlock?: undefined } | { errorStartingBlock: string } => { + console.log('STARTING BLOCK', JSON.stringify(block, null, 2)) - // TODO!!! turn this into a service and provide it + + const foundInCode = findTextInCode(block.orig, origFileContents) + if (typeof foundInCode === 'string') { + console.log('Apply error:', foundInCode, '; trying again.') + return { errorStartingBlock: foundInCode } + } + const [originalStart, originalEnd] = foundInCode + + let lineOffset = 0 + // compute line offset given multiple changes + for (let i = 0; i < blockNum; i += 1) { + const [diffAreaOriginalStart, diffAreaOriginalEnd] = diffAreaOriginalLines[i] + console.log('ROIGGINAL!!!', diffAreaOriginalStart, diffAreaOriginalEnd) + if (diffAreaOriginalStart > originalEnd) continue + + const diffareaid = diffareaidOfBlockNum[i] + const diffArea = this.diffAreaOfId[diffareaid] + + + const numNewLines = diffArea.endLine - diffArea.startLine + const numOldLines = diffAreaOriginalEnd - diffAreaOriginalStart + console.log('NUM NEW', numNewLines, numOldLines) + + lineOffset += numNewLines - numOldLines + } + + const startLine = originalStart + lineOffset + const endLine = originalEnd + lineOffset + console.log('adding to', startLine, endLine) + + const adding: Omit = { + type: 'DiffZone', + originalCode: block.orig, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), + } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) + + diffareaidOfBlockNum.push(diffZone.diffareaid) + diffAreaOriginalLines.push([originalStart, originalEnd]) + + latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + return { errorStartingBlock: undefined } + } + + + + + + + let shouldSendAnotherMessage = true + let nMessagesSent = 0 // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it - streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - useProviderFor: 'Apply', - logging: { loggingName: `generateSearchAndReplace` }, - messages, - onText: ({ fullText }) => { - const blocks = extractSearchReplaceBlocks(fullText) + while (shouldSendAnotherMessage) { + shouldSendAnotherMessage = false + nMessagesSent += 1 - for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: 'Apply', + logging: { loggingName: `generateSearchAndReplace` }, + messages, + onText: ({ fullText }) => { + const blocks = extractSearchReplaceBlocks(fullText) - if (block.state === 'done') - currStreamingBlockNum = blockNum + for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] - if (block.state === 'writingOriginal') // must be done writing original - continue + if (block.state === 'done') + currStreamingBlockNum = blockNum - // if should add new diffarea - if (!(blockNum in diffareaidOfBlockNum)) { - const foundInCode = findTextInCode(block.orig, fileContents) - if (typeof foundInCode === 'string') { - // TODO!!! log and retry - console.log('NOT FOUND IN CODE!!!!', foundInCode) + if (block.state === 'writingOriginal') // must be done writing original continue + + // if this is the first time we're seeing this block, add it as a diffarea + if (!(blockNum in diffareaidOfBlockNum)) { + console.log('FULLTEXT!!!!!\n', fullText) + const { errorStartingBlock } = onNewBlockStart(blockNum, block) + + if (errorStartingBlock) { + console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock) + + const errMsgForLLM = errorStartingBlock === 'Not found' ? + 'I interrupted you because the latest ORIGINAL code could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file.' + : errorStartingBlock === 'Not unique' ? + 'I interrupted you because the latest ORIGINAL code shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file.' + : '' + + messages.push( + { role: 'assistant', content: fullText }, // latest output + { role: 'user', content: errMsgForLLM } // user explanation of what's wrong + ) + if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) + + shouldSendAnotherMessage = true + revertAndContinueHistory() + return + } + } - const [startLine, endLine] = foundInCode + const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) + oldBlocks = blocks - const adding: Omit = { - type: 'DiffZone', - originalCode: block.orig, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), - } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) + // write new text to diffarea + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') continue - diffareaidOfBlockNum.push(diffZone.diffareaid) - latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + if (!latestStreamLocationMutable) continue + this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) + } // end for + this._refreshStylesAndDiffsInURI(uri) + }, + onFinalMessage: async ({ fullText }) => { + console.log('final message!!', fullText) + + // 1. wait 500ms and fix lint errors - call lint error workflow + // (update react state to say "Fixing errors") + const blocks = extractSearchReplaceBlocks(fullText) + + if (blocks.length === 0) { + this._notificationService.info(`Void: When running Apply, your model didn't output any changes we recognized. You might need to use a smarter model for Apply.`) } - const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) - oldBlocks = blocks - // write new text to diffarea - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue + for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') continue + this._writeText(uri, block.final, + { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + } + onDone(false) + }, + onError: (e) => { + console.log('ERROR in SearchReplace:', e.message) + onDone(e.message) + }, - if (!latestStreamLocationMutable) continue - this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) - } // end for + }) + } - this._refreshStylesAndDiffsInURI(uri) - }, - onFinalMessage: async ({ fullText }) => { - console.log('/* ON FINALMESSAGE */', fullText) - - // 1. wait 500ms and fix lint errors - call lint error workflow - // (update react state to say "Fixing errors") - const blocks = extractSearchReplaceBlocks(fullText) - - for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue - - this._writeText(uri, block.final, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - } - onDone(false) - }, - onError: (e) => { - console.log('ERROR', e); - onDone(true) - }, - - }) } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 2b840f83..a86e82b1 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -21,23 +21,25 @@ You are a coding assistant. You are given a list of instructions to follow \`INS Please respond to the user's query. The user's query is never invalid. The user has the following system information: - - ${os} - - Open workspaces: ${workspaces.join(', ')} +- ${os} +- Open workspaces: ${workspaces.join(', ')} In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes. For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer. - - Do not re-write the entire file in the code block. - - You can write comments like "// ... existing code" to indicate existing code. - - Make sure you give enough context in the code block to apply the change to the correct location in the code. +- Do not re-write the entire file in the code block. +- You can write comments like "// ... existing code" to indicate existing code. +- Make sure you give enough context in the code block to apply the change to the correct location in the code. You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. -If you are given tools, you are allowed to use them without asking for permission. You do not have to use them if you don't want to, but you may use them to gather context, etc. -If you are given tools, NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. +If you are given tools: +- You are allowed to use tools without asking for permission. +- Feel free to use tools to gather context, make suggestions, etc. +- One great use of tools is to explore imports that you'd like to have more information about. +- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below. - ## EXAMPLE 1 FILES math.ts @@ -249,9 +251,9 @@ The user's request may be "fuzzy" or not well-specified, and it is your job to i 2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make. For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name. - - Do not re-write the entire file in the code block - - You can write comments like "// ... existing code" to indicate existing code - - Make sure you give enough context in the code block to apply the changes to the correct location in the code` +- Do not re-write the entire file in the code block +- You can write comments like "// ... existing code" to indicate existing code +- Make sure you give enough context in the code block to apply the changes to the correct location in the code` export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService }) => { @@ -303,23 +305,28 @@ export const searchReplace_systemMessage = `\ You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file. A SEARCH/REPLACE block describes the code before and after a change. Here is the format: +${tripleTick[0]} ${ORIGINAL} // ... original code goes here ${DIVIDER} // ... final code goes here ${FINAL} +${tripleTick[1]} You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make. Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks. Directions: 1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. -2. The "original" code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. -3. The "original" code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. -4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. +2. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. +3. The original code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file. +4. The original code in each SEARCH/REPLACE block must be disjoint from all other blocks. + +The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. - Make sure you add all necessary imports. - Make sure the "final" code is complete and will not result in syntax/lint errors. -5. Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise. + +Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise. ## EXAMPLE 1 ORIGINAL_FILE diff --git a/src/vs/workbench/contrib/void/browser/searchReplaceService.ts b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts similarity index 75% rename from src/vs/workbench/contrib/void/browser/searchReplaceService.ts rename to src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts index 9bf958c4..e7a9448e 100644 --- a/src/vs/workbench/contrib/void/browser/searchReplaceService.ts +++ b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts @@ -8,6 +8,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; +import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js'; @@ -15,12 +16,10 @@ export interface ISearchReplaceService { readonly _serviceBrand: undefined; } -export const ISearchReplaceService = createDecorator('SearchReplaceService'); +export const ISearchReplaceService = createDecorator('SearchReplaceCacheService'); class SearchReplaceService extends Disposable implements ISearchReplaceService { _serviceBrand: undefined; - static readonly ID = 'SearchReplaceService'; - private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; @@ -28,8 +27,18 @@ class SearchReplaceService extends Disposable implements ISearchReplaceService { @ILLMMessageService private readonly llmMessageService: ILLMMessageService, ) { super() - // this.llmMessageService.sendLLMMessage({}) + } + send(params: Omit & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) { + this.llmMessageService.sendLLMMessage({ + ...params as ServiceSendLLMMessageParams, + onText: (p) => { + const { retry } = params.onText(p) + if (retry) { + + } + } + }) } } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 14371a22..06e708fd 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -552,7 +552,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName : '(never)', subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' : - undefined, + providerName === 'vLLM' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' : + undefined, } } else if (settingName === '_didFillInProviderSettings') { From baa89cc17d88379f6d0774ca6a1479dba1560cdc Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 18 Feb 2025 14:14:38 -0800 Subject: [PATCH 48/92] dummy marker service --- .../void/browser/MarkerCheckService.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/vs/workbench/contrib/void/browser/MarkerCheckService.ts diff --git a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts new file mode 100644 index 00000000..f41c0513 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts @@ -0,0 +1,119 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js'; + +export interface IMarkerCheckService { + readonly _serviceBrand: undefined; +} + +export const IMarkerCheckService = createDecorator('markerCheckService'); + +class MarkerCheckService extends Disposable implements IMarkerCheckService { + _serviceBrand: undefined; + + constructor( + @IMarkerService private readonly _markerService: IMarkerService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @ITextModelService private readonly _textModelService: ITextModelService, + ) { + super(); + + setInterval(async () => { + const allMarkers = this._markerService.read(); + const errors = allMarkers.filter(marker => marker.severity === MarkerSeverity.Error); + + if (errors.length > 0) { + for (const error of errors) { + + console.log(`----------------------------------------------`); + + console.log(`${error.resource.toString()}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file + + try { + // Get the text model for the file + const modelReference = await this._textModelService.createModelReference(error.resource); + const model = modelReference.object.textEditorModel; + + // Create a range from the marker + const range = new Range( + error.startLineNumber, + error.startColumn, + error.endLineNumber, + error.endColumn + ); + + // Get code action providers for this model + const codeActionProvider = this._languageFeaturesService.codeActionProvider; + const providers = codeActionProvider.ordered(model); + + if (providers.length > 0) { + // Request code actions from each provider + for (const provider of providers) { + const context: CodeActionContext = { + trigger: CodeActionTriggerType.Invoke, // keeping 'trigger' since it works + only: 'quickfix' // adding this to filter for quick fixes + }; + + const actions = await provider.provideCodeActions( + model, + range, + context, + CancellationToken.None + ); + + if (actions?.actions?.length) { + + const quickFixes = actions.actions.filter(action => action.isPreferred); // ! all quickFixes for the error + const quickFixesForImports = actions.actions.filter(action => action.isPreferred && action.title.includes('import')); // ! all possible imports + quickFixesForImports + + if (quickFixes.length > 0) { + console.log('Available Quick Fixes:'); + quickFixes.forEach(action => { + console.log(`- ${action.title}`); + }); + } + } + } + } + + // Dispose the model reference + modelReference.dispose(); + } catch (e) { + console.error('Error getting quick fixes:', e); + } + } + } + }, 5000); + } + + + // private _onMarkersChanged = (changedResources: readonly URI[]): void => { + // for (const resource of changedResources) { + // const markers = this._markerService.read({ resource }); + + // if (markers.length === 0) { + // console.log(`${resource.toString()}: No diagnostics`); + // continue; + // } + + // console.log(`Diagnostics for ${resource.toString()}:`); + // markers.forEach(marker => this._logMarker(marker)); + // } + // }; + + +} + +registerSingleton(IMarkerCheckService, MarkerCheckService, InstantiationType.Eager); From 5699cf19f44ff11caad41208e73c9801cdf24463 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Feb 2025 16:12:16 -0800 Subject: [PATCH 49/92] merge updates --- .../contrib/void/browser/editCodeService.ts | 2 ++ .../contrib/void/common/llmMessageTypes.ts | 9 +++++---- .../void/electron-main/llmMessage/anthropic.ts | 9 ++++++++- .../void/electron-main/llmMessage/openai.ts | 14 ++++++++++---- .../llmMessage/postprocessToolCalls.ts | 10 ++++++++++ .../llmMessage/preprocessLLMMessages.ts | 3 --- 6 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index ad3eb81d..e3a5d998 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -42,6 +42,7 @@ import { ILLMMessageService } from '../common/llmMessageService.js'; import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { VSReadFile } from './helpers/readFile.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -254,6 +255,7 @@ class EditCodeService extends Disposable implements IEditCodeService { @IMetricsService private readonly _metricsService: IMetricsService, @INotificationService private readonly _notificationService: INotificationService, @ICommandService private readonly _commandService: ICommandService, + @IFileService private readonly _fileService: IFileService, ) { super(); diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 75cf2739..0956b08b 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import { ChatMessage } from '../browser/chatThreadService.js' -import { InternalToolInfo } from './toolsService.js' +import { InternalToolInfo, ToolName } from './toolsService.js' import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' @@ -37,15 +37,16 @@ export type LLMChatMessage = { id: string; } -export type LLMToolCallType = { - name: string; + +export type ToolCallType = { + name: ToolName; params: string; id: string; } export type OnText = (p: { newText: string, fullText: string }) => void -export type OnFinalMessage = (p: { fullText: string, toolCalls?: LLMToolCallType[] }) => void // id is tool_use_id +export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index 8308bb44..c4338ebb 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -8,6 +8,7 @@ import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes. import { anthropicMaxPossibleTokens, developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; import { InternalToolInfo } from '../../common/toolsService.js'; import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; +import { isAToolName } from './postprocessToolCalls.js'; @@ -86,7 +87,13 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: stream.on('finalMessage', (response) => { // stringify the response's content const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - const toolCalls = response.content.map(c => c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null).filter(c => !!c) + const toolCalls = response.content + .map(c => { + if (c.type !== 'tool_use') return null + if (!isAToolName(c.name)) return null + return c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null + }) + .filter(t => !!t) onFinalMessage({ fullText: content, toolCalls }) }) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 80db8d73..66c0ffe1 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -9,6 +9,7 @@ import { Model } from 'openai/resources/models.js'; import { InternalToolInfo } from '../../common/toolsService.js'; import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; +import { isAToolName } from './postprocessToolCalls.js'; // import { parseMaxTokensStr } from './util.js'; @@ -205,10 +206,15 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: me onText({ newText, fullText }); } onFinalMessage({ - fullText, toolCalls: Object.keys(toolCallOfIndex).map(index => { - const tool = toolCallOfIndex[index] - return { name: tool.name, id: tool.id, params: tool.params } - }) + fullText, + toolCalls: Object.keys(toolCallOfIndex) + .map(index => { + const tool = toolCallOfIndex[index] + if (isAToolName(tool.name)) + return { name: tool.name, id: tool.id, params: tool.params } + return null + }) + .filter(t => !!t) }); }) // when error/fail - this catches errors of both .create() and .then(for await) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts new file mode 100644 index 00000000..e9bb2b7c --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts @@ -0,0 +1,10 @@ +import { ToolName, toolNames } from '../../common/toolsService'; + + + +const toolNamesSet = new Set(toolNames) + +export const isAToolName = (toolName: string): toolName is ToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index eba90468..689e44de 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -262,9 +262,6 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: - - - /* From ac1788ae9a7fbf034ec58d0a7f5dffba6be444ff Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Feb 2025 21:25:23 -0800 Subject: [PATCH 50/92] tool pages work, improve prompt --- .../contrib/void/browser/chatThreadService.ts | 2 +- .../contrib/void/browser/prompt/prompts.ts | 24 +++++--- .../react/src/markdown/ChatMarkdownRender.tsx | 4 +- .../contrib/void/common/toolsService.ts | 61 ++++++++++++++----- .../llmMessage/postprocessToolCalls.ts | 2 +- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 7f53a043..fd7e4288 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -356,7 +356,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // add user's message to chat history const instructions = userMessage - const userMessageContent = await chat_userMessageContent(instructions, currSelns, currSelns) + const userMessageContent = await chat_userMessageContent(instructions, currSelns) const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._modelService, this._fileService) const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 1e9dd2a9..f04fcb5c 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -33,10 +33,12 @@ For example, if the user asks you to "make this file look nicer", make sure your You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. If you are given tools: +- Only use tools if the user asks you to do something. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools. - You are allowed to use tools without asking for permission. - Feel free to use tools to gather context, make suggestions, etc. - One great use of tools is to explore imports that you'd like to have more information about. -- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. +- Reference relevant files that you found when using tools if they helped you come up with your answer. +- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not even refer to "pages" of results, just say you're getting more results. Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below. @@ -176,14 +178,14 @@ const stringifyFileSelections = async (fileSelections: FileSelection[], modelSer return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') } const stringifyCodeSelections = (codeSelections: CodeSelection[]) => { - return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') + return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') || null } const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => { if (!currSelns) return '' return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n') } -export const chat_userMessageContent = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null) => { +export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => { const selnsStr = stringifySelectionNames(currSelns) @@ -198,6 +200,8 @@ export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | // ADD IN FILES AT TOP const allSelections = [...currSelns || [], ...prevSelns || []] + if (allSelections.length === 0) return null + const codeSelections: CodeSelection[] = [] const fileSelections: FileSelection[] = [] const filesURIs = new Set() @@ -219,17 +223,17 @@ export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService) const selnsStr = stringifyCodeSelections(codeSelections) - let str = '' - str += 'ALL FILE CONTENTS\n' - if (filesStr) str += `${filesStr}\n` - if (selnsStr) str += `${selnsStr}\n` + if (filesStr || selnsStr) return `\ +ALL FILE CONTENTS +${filesStr} +${selnsStr}` - return str; + return null } -export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | undefined) => { - if (userMessage) return `${userMessage}\n${selectionsString}\n` +export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | null) => { + if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}` else return userMessage } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 5de08900..3e77a6df 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -29,7 +29,7 @@ const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => -const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => { +const ApplyButtonsOnHover = ({ applyStr }: { applyStr: string }) => { const accessor = useAccessor() const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) @@ -120,7 +120,7 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati return } + buttonsOnHover={applyBoxId && } /> } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 7620b2cd..4a0933a2 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -1,7 +1,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js' import { URI } from '../../../../base/common/uri.js' import { IModelService } from '../../../../editor/common/services/model.js' -import { IFileService, IFileStat } from '../../../../platform/files/common/files.js' +import { IFileService } from '../../../../platform/files/common/files.js' import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js' @@ -93,22 +93,46 @@ export type ToolCallReturnType export type ToolFns = { [T in ToolName]: (p: string) => Promise> } export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType) => string } +const MAX_DEPTH = 1 +const MAX_CHILDREN = 500 +async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, pageNumber: number): Promise { + let output = ''; -async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise { - let output = '' - function traverseChildren(children: IFileStat[], depth: number) { - const indentation = ' '.repeat(depth); - for (const child of children) { - output += `${indentation}- ${child.name}\n`; - traverseChildren(child.children ?? [], depth + 1); + const indentation = (depth: number, isLast: boolean): string => { + if (depth === 0) return ''; + return `${'| '.repeat(depth - 1)}${isLast ? '└── ' : '├── '}`; + }; + + async function traverseChildren(uri: URI, depth: number, isLast: boolean) { + const stat = await fileService.resolve(uri, { resolveMetadata: false }); + + if ((depth === 0 && pageNumber === 1) || depth !== 0) + output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`; // TODO say where symlink links to + + // list children + const originalChildrenLength = stat.children?.length ?? 0 + const fromChildIdx = MAX_CHILDREN * (pageNumber - 1) + const toChildIdx = MAX_CHILDREN * pageNumber - 1 // INCLUSIVE + const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; + + if (!stat.isDirectory) return; + + if (listChildren.length === 0) return + if (depth === MAX_DEPTH) return // right now MAX_DEPTH=1 to make pagination work nicely + + for (let i = 0; i < Math.min(listChildren.length, MAX_CHILDREN); i++) { + await traverseChildren(listChildren[i].resource, depth + 1, i === listChildren.length - 1); } + const nCutoffChildren = (originalChildrenLength - 1) - toChildIdx + if (nCutoffChildren > 0) { + output += `${indentation(depth + 1, true)}(${nCutoffChildren} results remaining...)\n` + } + } - const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); - // kickstart recursion - output += `${stat.name}\n`; - traverseChildren(stat.children ?? [], 1); + await traverseChildren(rootURI, 0, false); + console.log('OUTPUT', output); return output; } @@ -119,6 +143,12 @@ const validateURI = (uriStr: unknown) => { return uri } +const validatePageNum = (pageNumberUnknown: unknown) => { + const proposedPageNum = Number.parseInt(pageNumberUnknown + '') + const num = Number.isInteger(proposedPageNum) ? proposedPageNum : 1 + const pageNumber = num < 1 ? 1 : num + return pageNumber +} export interface IToolsService { readonly _serviceBrand: undefined; toolFns: ToolFns; @@ -170,12 +200,13 @@ export class ToolsService implements IToolsService { list_dir: async (s: string) => { const o = parseObj(s) if (!o) return invalidToolParamMsg - const { uri: uriStr } = o + const { uri: uriStr, pageNumber: pageNumberUnknown } = o const uri = validateURI(uriStr) + const pageNumber = validatePageNum(pageNumberUnknown) + // TODO!!!! check to make sure in workspace - // TODO check to make sure is not gitignored - const treeStr = await generateDirectoryTreeMd(fileService, uri) + const treeStr = await generateDirectoryTreeMd(fileService, uri, pageNumber) return treeStr }, pathname_search: async (s: string) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts index e9bb2b7c..2feeeb80 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts @@ -1,4 +1,4 @@ -import { ToolName, toolNames } from '../../common/toolsService'; +import { ToolName, toolNames } from '../../common/toolsService.js'; From 02f64b7ff61c2bc6265e7f375b110448c833aba4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Feb 2025 22:28:04 -0800 Subject: [PATCH 51/92] finish tool pagination --- .../contrib/void/browser/chatThreadService.ts | 7 +- .../contrib/void/common/toolsService.ts | 152 +++++++++++------- 2 files changed, 100 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index fd7e4288..cc875a79 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -78,7 +78,8 @@ export type ChatMessage = role: 'assistant'; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored - } | ToolMessage + } + | ToolMessage type UserMessageType = ChatMessage & { role: 'user' } type UserMessageState = UserMessageType['state'] @@ -422,8 +423,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 1. let toolResult: Awaited> + let toolResultVal: ToolCallReturnType try { toolResult = await this._toolsService.toolFns[toolName](tool.params) + toolResultVal = toolResult[0] } catch (error) { this._setStreamState(threadId, { error }) shouldSendAnotherMessage = false @@ -440,7 +443,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { break } - this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResult, }) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResultVal, }) shouldSendAnotherMessage = true } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 4a0933a2..3b609f60 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -90,12 +90,16 @@ export type ToolCallReturnType : T extends 'search' ? string | URI[] : never -export type ToolFns = { [T in ToolName]: (p: string) => Promise> } -export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType) => string } +export type ToolFns = { [T in ToolName]: (p: string) => Promise<[ToolCallReturnType, boolean]> } +export type ToolResultToString = { [T in ToolName]: (result: [ToolCallReturnType, boolean]) => string } + + +// pagination info +const MAX_FILE_CHARS_PAGE = 50_000 +const MAX_CHILDREN_URIs_PAGE = 500 const MAX_DEPTH = 1 -const MAX_CHILDREN = 500 -async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, pageNumber: number): Promise { +async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, pageNumber: number): Promise<[string, boolean]> { let output = ''; const indentation = (depth: number, isLast: boolean): string => { @@ -103,16 +107,19 @@ async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, return `${'| '.repeat(depth - 1)}${isLast ? '└── ' : '├── '}`; }; + let hasNextPage = false + async function traverseChildren(uri: URI, depth: number, isLast: boolean) { const stat = await fileService.resolve(uri, { resolveMetadata: false }); + // we might want to say where symlink links to if ((depth === 0 && pageNumber === 1) || depth !== 0) - output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`; // TODO say where symlink links to + output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`; // list children const originalChildrenLength = stat.children?.length ?? 0 - const fromChildIdx = MAX_CHILDREN * (pageNumber - 1) - const toChildIdx = MAX_CHILDREN * pageNumber - 1 // INCLUSIVE + const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1) + const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 // INCLUSIVE const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; if (!stat.isDirectory) return; @@ -120,25 +127,43 @@ async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, if (listChildren.length === 0) return if (depth === MAX_DEPTH) return // right now MAX_DEPTH=1 to make pagination work nicely - for (let i = 0; i < Math.min(listChildren.length, MAX_CHILDREN); i++) { + for (let i = 0; i < Math.min(listChildren.length, MAX_CHILDREN_URIs_PAGE); i++) { await traverseChildren(listChildren[i].resource, depth + 1, i === listChildren.length - 1); } - const nCutoffChildren = (originalChildrenLength - 1) - toChildIdx - if (nCutoffChildren > 0) { - output += `${indentation(depth + 1, true)}(${nCutoffChildren} results remaining...)\n` + const nCutoffResults = (originalChildrenLength - 1) - toChildIdx + if (nCutoffResults >= 1) { + output += `${indentation(depth + 1, true)}(${nCutoffResults} results remaining...)\n` + hasNextPage = true } } await traverseChildren(rootURI, 0, false); - console.log('OUTPUT', output); - return output; + return [output, hasNextPage] +} + + +const validateJSON = (s: string): { [s: string]: unknown } => { + try { + const o = JSON.parse(s) + return o + } + catch (e) { + throw new Error(`Tool parameter was not a valid JSON: "${s}".`) + } +} + + + +const validateQueryStr = (queryStr: unknown) => { + if (typeof queryStr !== 'string') throw new Error('Error calling tool: provided query must be a string.') + return queryStr } const validateURI = (uriStr: unknown) => { - if (typeof uriStr !== 'string') throw new Error('(provided uri must be a string)') + if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.') const uri = URI.file(uriStr) return uri } @@ -173,83 +198,96 @@ export class ToolsService implements IToolsService { @IInstantiationService instantiationService: IInstantiationService, ) { - const queryBuilder = instantiationService.createInstance(QueryBuilder); - const parseObj = (s: string): { [s: string]: unknown } | null => { - try { - const o = JSON.parse(s) - return o - } - catch (e) { - return null - } - } - - const invalidToolParamMsg = '(LLM parameter format was invalid for this tool)' this.toolFns = { read_file: async (s: string) => { - const o = parseObj(s) - if (!o) return invalidToolParamMsg - const { uri: uriStr } = o + const o = validateJSON(s) + const { uri: uriStr, pageNumber: pageNumberUnknown } = o const uri = validateURI(uriStr) - const fileContents = await VSReadFile(uri, modelService, fileService) - return fileContents ?? invalidToolParamMsg + const pageNumber = validatePageNum(pageNumberUnknown) + + const readFileContents = await VSReadFile(uri, modelService, fileService) + + const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) + const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 + let fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate + const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 + + return [fileContents || '(empty)', hasNextPage] }, list_dir: async (s: string) => { - const o = parseObj(s) - if (!o) return invalidToolParamMsg + const o = validateJSON(s) const { uri: uriStr, pageNumber: pageNumberUnknown } = o const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) // TODO!!!! check to make sure in workspace - const treeStr = await generateDirectoryTreeMd(fileService, uri, pageNumber) - return treeStr + const [treeStr, hasNextPage] = await generateDirectoryTreeMd(fileService, uri, pageNumber) + return [treeStr, hasNextPage] }, pathname_search: async (s: string) => { - const o = parseObj(s) - if (!o) return invalidToolParamMsg - const { query: queryStr } = o + const o = validateJSON(s) + const { query: queryUnknown, pageNumber: pageNumberUnknown } = o + + const queryStr = validateQueryStr(queryUnknown) + const pageNumber = validatePageNum(pageNumberUnknown) - if (typeof queryStr !== 'string') return 'Error: query was not a string' const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }) - const data = await searchService.fileSearch(query, CancellationToken.None) - const URIs = data.results.map(({ resource, results }) => resource) - return URIs + + const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1) + const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 + const URIs = data.results + .slice(fromIdx, toIdx + 1) // paginate + .map(({ resource, results }) => resource) + + const hasNextPage = (data.results.length - 1) - toIdx >= 1 + + return [URIs, hasNextPage] }, search: async (s: string) => { - const o = parseObj(s) - if (!o) return '(could not search)' - const { query: queryStr } = o + const o = validateJSON(s) + const { query: queryUnknown, pageNumber: pageNumberUnknown } = o + + const queryStr = validateQueryStr(queryUnknown) + const pageNumber = validatePageNum(pageNumberUnknown) - if (typeof queryStr !== 'string') return 'Error: query was not a string' const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)) - const data = await searchService.textSearch(query, CancellationToken.None) - const URIs = data.results.map(({ resource, results }) => resource) - return URIs + + const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1) + const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 + const URIs = data.results + .slice(fromIdx, toIdx + 1) // paginate + .map(({ resource, results }) => resource) + + const hasNextPage = (data.results.length - 1) - toIdx >= 1 + + return [URIs, hasNextPage] }, } + + const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : '' + this.toolResultToString = { - read_file: (URIs) => { - return URIs + read_file: ([fileContents, hasNextPage]) => { + return fileContents + nextPageStr(hasNextPage) }, - list_dir: (URIs) => { - return URIs + list_dir: ([dirTreeStr, hasNextPage]) => { + return dirTreeStr + nextPageStr(hasNextPage) }, - pathname_search: (URIs) => { + pathname_search: ([URIs, hasNextPage]) => { if (typeof URIs === 'string') return URIs - return URIs.map(uri => uri.fsPath).join('\n') + return URIs.map(uri => uri.fsPath).join('\n') + nextPageStr(hasNextPage) }, - search: (URIs) => { + search: ([URIs, hasNextPage]) => { if (typeof URIs === 'string') return URIs - return URIs.map(uri => uri.fsPath).join('\n') + return URIs.map(uri => uri.fsPath).join('\n') + nextPageStr(hasNextPage) }, } From 858b6f6a91429d8c88b89cc905bcd39626e2ac4b Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 19 Feb 2025 00:10:44 -0800 Subject: [PATCH 52/92] file service read --- .../void/browser/MarkerCheckService.ts | 16 +++ .../void/browser/autocompleteService.ts | 5 +- .../contrib/void/browser/chatThreadService.ts | 8 +- .../contrib/void/browser/editCodeService.ts | 7 +- .../contrib/void/browser/prompt/prompts.ts | 15 ++- .../contrib/void/common/toolsService.ts | 5 +- .../contrib/void/common/voidFileService.ts | 109 ++++++++++++++++++ .../llmMessage/postprocessToolCalls.ts | 2 - 8 files changed, 142 insertions(+), 25 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/voidFileService.ts diff --git a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts index f41c0513..7cdcb1e2 100644 --- a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts +++ b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts @@ -12,6 +12,7 @@ import { ITextModelService } from '../../../../editor/common/services/resolverSe import { Range } from '../../../../editor/common/core/range.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js'; +import { URI } from '../../../../base/common/uri.js'; export interface IMarkerCheckService { readonly _serviceBrand: undefined; @@ -99,6 +100,21 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService { } + + + fixErrorsInFiles(uris: URI[], contextSoFar: []) { + const allMarkers = this._markerService.read(); + + + // check errors in files + + + // give LLM errors in files + + + + } + // private _onMarkersChanged = (changedResources: readonly URI[]): void => { // for (const resource of changedResources) { // const markers = this._markerService.read({ resource }); diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index aa8902f3..5fc8ac76 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -16,9 +16,9 @@ import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; -import { isWindows } from '../../../../base/common/platform.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; +import { _ln, allLinebreakSymbols } from '../common/voidFileService.js'; // import { IContextGatheringService } from './contextGatheringService.js'; // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts @@ -415,9 +415,6 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS // } -const allLinebreakSymbols = ['\r\n', '\n'] -const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1] - type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string } const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => { diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index cc875a79..8bbab4ad 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -12,12 +12,11 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from './prompt/prompts.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js'; import { toLLMChatMessage } from '../common/llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IVoidFileService } from '../common/voidFileService.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -189,8 +188,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { constructor( @IStorageService private readonly _storageService: IStorageService, - @IModelService private readonly _modelService: IModelService, - @IFileService private readonly _fileService: IFileService, + @IVoidFileService private readonly _voidFileService: IVoidFileService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IToolsService private readonly _toolsService: IToolsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @@ -358,7 +356,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // add user's message to chat history const instructions = userMessage const userMessageContent = await chat_userMessageContent(instructions, currSelns) - const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._modelService, this._fileService) + const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr) const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index e3a5d998..02e3fc23 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -41,8 +41,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ILLMMessageService } from '../common/llmMessageService.js'; import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; -import { VSReadFile } from './helpers/readFile.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { IVoidFileService } from '../common/voidFileService.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -255,7 +254,7 @@ class EditCodeService extends Disposable implements IEditCodeService { @IMetricsService private readonly _metricsService: IMetricsService, @INotificationService private readonly _notificationService: INotificationService, @ICommandService private readonly _commandService: ICommandService, - @IFileService private readonly _fileService: IFileService, + @IVoidFileService private readonly _voidFileService: IVoidFileService, ) { super(); @@ -1184,7 +1183,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = uri_ // generate search/replace block text - const origFileContents = await VSReadFile(uri, this._modelService, this._fileService) + const origFileContents = await this._voidFileService.readFile(uri) if (origFileContents === null) return diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index f04fcb5c..90e01d50 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -7,10 +7,9 @@ import { URI } from '../../../../../base/common/uri.js'; import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; -import { VSReadFile } from '../helpers/readFile.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { os } from '../helpers/systemInfo.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IVoidFileService } from '../../common/voidFileService.js'; // this is just for ease of readability @@ -169,10 +168,10 @@ ${tripleTick[1]} } const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.' -const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => { +const stringifyFileSelections = async (fileSelections: FileSelection[], voidFileService: IVoidFileService) => { if (fileSelections.length === 0) return null const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => { - const content = await VSReadFile(sel.fileURI, modelService, fileService) ?? failToReadStr + const content = await voidFileService.readFile(sel.fileURI) ?? failToReadStr return { ...sel, content } })) return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') @@ -195,7 +194,7 @@ export const chat_userMessageContent = async (instructions: string, currSelns: S return str; }; -export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => { +export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, voidFileService: IVoidFileService) => { // ADD IN FILES AT TOP const allSelections = [...currSelns || [], ...prevSelns || []] @@ -220,7 +219,7 @@ export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | } } - const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService) + const filesStr = await stringifyFileSelections(fileSelections, voidFileService) const selnsStr = stringifyCodeSelections(codeSelections) @@ -297,12 +296,12 @@ For example, if the user is asking you to "make this variable a better name", ma - Make sure you give enough context in the code block to apply the changes to the correct location in the code` -export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService, fileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, fileService: IFileService }) => { +export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => { // we may want to do this in batches const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null } - const file = await stringifyFileSelections([fileSelection], modelService, fileService) + const file = await stringifyFileSelections([fileSelection], voidFileService) return `\ ## FILE diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 3b609f60..fadbf333 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -5,9 +5,9 @@ import { IFileService } from '../../../../platform/files/common/files.js' import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js' -import { VSReadFile } from '../../../../workbench/contrib/void/browser/helpers/readFile.js' import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js' import { ISearchService } from '../../../../workbench/services/search/common/search.js' +import { IVoidFileService } from './voidFileService.js' // tool use for AI @@ -196,6 +196,7 @@ export class ToolsService implements IToolsService { @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @ISearchService searchService: ISearchService, @IInstantiationService instantiationService: IInstantiationService, + @IVoidFileService voidFileService: IVoidFileService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -208,7 +209,7 @@ export class ToolsService implements IToolsService { const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - const readFileContents = await VSReadFile(uri, modelService, fileService) + const readFileContents = await voidFileService.readFile(uri) const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 diff --git a/src/vs/workbench/contrib/void/common/voidFileService.ts b/src/vs/workbench/contrib/void/common/voidFileService.ts new file mode 100644 index 00000000..668f1869 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/voidFileService.ts @@ -0,0 +1,109 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { isWindows } from '../../../../base/common/platform.js'; +import { URI } from '../../../../base/common/uri.js'; +import { EndOfLinePreference } from '../../../../editor/common/model.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + + +// linebreak symbols +export const allLinebreakSymbols = ['\r\n', '\n'] +export const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1] + +export interface IVoidFileService { + readonly _serviceBrand: undefined; + + readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise; + +} + +export const IVoidFileService = createDecorator('VoidFileService'); + +// implemented by calling channel +export class VoidFileService implements IVoidFileService { + readonly _serviceBrand: undefined; + + constructor( + @IModelService private readonly modelService: IModelService, + @IFileService private readonly fileService: IFileService, + ) { + + } + + readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + + // attempt to read the model + const modelResult = await this._readModel(uri, range); + if (modelResult) return modelResult; + + // if no model, read the raw file + const fileResult = await this._readFileRaw(uri, range); + if (fileResult) return fileResult; + + return ''; + } + + _readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + + try { // this throws an error if no file exists (eg it was deleted) + + const res = await this.fileService.readFile(uri); + + if (range) { + return res.value.toString() + .split(_ln) + .slice(range.startLineNumber - 1, range.endLineNumber) + .join(_ln) + } + + return res.value.toString(); + + + } catch (e) { + return null; + } + } + + + _readModel = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + + // read saved model (sometimes null if the user reloads application) + let model = this.modelService.getModel(uri); + + // check all opened models for the same `fsPath` + if (!model) { + const models = this.modelService.getModels(); + for (const m of models) { + if (m.uri.fsPath === uri.fsPath) { + model = m + break; + } + } + } + + // if still not found, return + if (!model) { return null } + + // if range, read it + if (range) { + return model.getValueInRange({ + startLineNumber: range.startLineNumber, + endLineNumber: range.endLineNumber, + startColumn: 1, + endColumn: Number.MAX_VALUE + }, EndOfLinePreference.LF); + } else { + return model.getValue(EndOfLinePreference.LF) + } + + } + +} + +registerSingleton(IVoidFileService, VoidFileService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts index 2feeeb80..aee52dcb 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts @@ -1,7 +1,5 @@ import { ToolName, toolNames } from '../../common/toolsService.js'; - - const toolNamesSet = new Set(toolNames) export const isAToolName = (toolName: string): toolName is ToolName => { From 6a44d668db6222febf074a587c01f462e92d0010 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Feb 2025 00:16:23 -0800 Subject: [PATCH 53/92] + --- src/vs/workbench/contrib/void/common/toolsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index fadbf333..9d759199 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -162,6 +162,7 @@ const validateQueryStr = (queryStr: unknown) => { } +// TODO!!!! check to make sure in workspace const validateURI = (uriStr: unknown) => { if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.') const uri = URI.file(uriStr) @@ -225,7 +226,6 @@ export class ToolsService implements IToolsService { const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - // TODO!!!! check to make sure in workspace const [treeStr, hasNextPage] = await generateDirectoryTreeMd(fileService, uri, pageNumber) return [treeStr, hasNextPage] }, From 0f26ee2288481a92348febc39d3f6d1de6d42ff4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Feb 2025 00:47:03 -0800 Subject: [PATCH 54/92] misc fixes --- src/vs/workbench/contrib/void/browser/MarkerCheckService.ts | 2 +- src/vs/workbench/contrib/void/common/toolsService.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts index 7cdcb1e2..4ea36c4c 100644 --- a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts +++ b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts @@ -103,7 +103,7 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService { fixErrorsInFiles(uris: URI[], contextSoFar: []) { - const allMarkers = this._markerService.read(); + // const allMarkers = this._markerService.read(); // check errors in files diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 9d759199..41ab059b 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -1,6 +1,5 @@ import { CancellationToken } from '../../../../base/common/cancellation.js' import { URI } from '../../../../base/common/uri.js' -import { IModelService } from '../../../../editor/common/services/model.js' import { IFileService } from '../../../../platform/files/common/files.js' import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' @@ -113,7 +112,9 @@ async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, const stat = await fileService.resolve(uri, { resolveMetadata: false }); // we might want to say where symlink links to - if ((depth === 0 && pageNumber === 1) || depth !== 0) + if (depth === 0 && pageNumber !== 1) + output += '' + else output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`; // list children @@ -193,7 +194,6 @@ export class ToolsService implements IToolsService { constructor( @IFileService fileService: IFileService, - @IModelService modelService: IModelService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @ISearchService searchService: ISearchService, @IInstantiationService instantiationService: IInstantiationService, From 62c2622ced39d4839017056c1ce02233bf5613fa Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Feb 2025 17:57:40 -0800 Subject: [PATCH 55/92] Void's settings --- .../contrib/void/browser/editCodeService.ts | 2 +- .../react/src/markdown/ChatMarkdownRender.tsx | 18 ++++++++++++++++++ .../react/src/void-settings-tsx/Settings.tsx | 2 +- .../contrib/void/browser/sidebarActions.ts | 2 +- .../contrib/void/browser/voidSettingsPane.ts | 6 +++--- .../contrib/void/common/llmMessageService.ts | 6 +++--- .../contrib/void/common/toolsService.ts | 3 ++- .../electron-main/llmMessage/sendLLMMessage.ts | 2 +- 8 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 02e3fc23..c7629f89 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1590,7 +1590,7 @@ class EditCodeService extends Disposable implements IEditCodeService { secondary: [{ id: 'void.onerror.opensettings', enabled: true, - label: 'Open Void settings', + label: `Open Void's settings`, tooltip: '', class: undefined, run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 3e77a6df..7a9953b7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -201,6 +201,24 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati ))} ) + // attempt at indentation + // return ( + // + // {t.items.map((item, index) => ( + //
  • + // {item.task && ( + // + // )} + // + // + // + //
  • + // ))} + //
    + // ) } if (t.type === "paragraph") { diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 0cfdef04..fd612c43 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -624,7 +624,7 @@ export const Settings = () => {
    -

    Void Settings

    +

    {`Void's Settings`}

    {/* separator */}
    diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 2e64c53f..39eb8381 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -241,7 +241,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'void.settingsAction', - title: 'Void Settings', + title: `Void's Settings`, icon: { id: 'settings-gear' }, menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }] }); diff --git a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts index 0fd8ce2e..d5e99d57 100644 --- a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts +++ b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts @@ -49,7 +49,7 @@ class VoidSettingsInput extends EditorInput { } override getName(): string { - return nls.localize('voidSettingsInputsName', 'Void Settings'); + return nls.localize('voidSettingsInputsName', 'Void\'s Settings'); } override getIcon() { @@ -112,7 +112,7 @@ class VoidSettingsPane extends EditorPane { // register Settings pane Registry.as(EditorExtensions.EditorPane).registerEditorPane( - EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void Settings Pane")), + EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void\'s Settings Pane")), [new SyncDescriptor(VoidSettingsInput)] ); @@ -202,7 +202,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '0_command', command: { id: VOID_TOGGLE_SETTINGS_ACTION_ID, - title: nls.localize('voidSettings', "Void Settings") + title: nls.localize('voidSettings', "Void\'s Settings") }, order: 1 }); diff --git a/src/vs/workbench/contrib/void/common/llmMessageService.ts b/src/vs/workbench/contrib/void/common/llmMessageService.ts index 314031d4..bb6cf09c 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageService.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageService.ts @@ -99,15 +99,15 @@ export class LLMMessageService extends Disposable implements ILLMMessageService let message: string if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected') - message = `Please add a provider in Void Settings.` + message = `Please add a provider in Void's Settings.` else if (isDisabled === 'addModel') message = `Please add a model.` else if (isDisabled === 'needToEnableModel') message = `Please enable a model.` else if (isDisabled === 'notFilledIn') - message = `Please fill in Void Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.` + message = `Please fill in Void's Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.` else - message = 'Please add a provider in Void Settings.' + message = `Please add a provider in Void's Settings.` onError({ message, fullError: null }) return null diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 41ab059b..dbdd0e15 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -23,7 +23,6 @@ export type InternalToolInfo = { required: string[], // required paramNames } -// helper const paginationHelper = { desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } @@ -166,6 +165,7 @@ const validateQueryStr = (queryStr: unknown) => { // TODO!!!! check to make sure in workspace const validateURI = (uriStr: unknown) => { if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.') + const uri = URI.file(uriStr) return uri } @@ -270,6 +270,7 @@ export class ToolsService implements IToolsService { return [URIs, hasNextPage] }, + } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 8e29bff4..0a182aec 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -74,7 +74,7 @@ export const sendLLMMessage = ({ // handle failed to fetch errors, which give 0 information by design if (error === 'TypeError: fetch failed') - error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void Settings, or your local model provider like Ollama is powered off.` + error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.` captureLLMEvent(`${loggingName} - Error`, { error }) onError_({ message: error, fullError }) From e14aab632d282fe0ea7338b72b66f57e0d4cf654 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 01:21:28 -0800 Subject: [PATCH 56/92] multiple find/replace blocks almost work. only 1 diffarea per find/replace application --- .../parts/editor/editorGroupWatermark.ts | 2 +- .../contrib/void/browser/chatThreadService.ts | 5 +- .../contrib/void/browser/editCodeService.ts | 339 ++++++++++-------- 3 files changed, 188 insertions(+), 158 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 42f4a7a7..8c0ebe8b 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -270,7 +270,7 @@ export class EditorGroupWatermark extends Disposable { const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings'); const button3 = append(recentsBox, $('button')); - button3.textContent = 'Void Settings' + button3.textContent = `Void's Settings` button3.style.display = 'block' button3.style.marginLeft = 'auto' button3.style.marginRight = 'auto' diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 8bbab4ad..05ef405b 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -72,8 +72,7 @@ export type ChatMessage = stagingSelections: StagingSelectionItem[]; isBeingEdited: boolean; } - } - | { + } | { role: 'assistant'; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored @@ -85,7 +84,7 @@ type UserMessageState = UserMessageType['state'] export const defaultMessageState: UserMessageState = { stagingSelections: [], - isBeingEdited: false + isBeingEdited: false, } // a 'thread' means a chat message history diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index c7629f89..e142e519 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -39,7 +39,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; -import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; +import { LLMChatMessage, OnError, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { IVoidFileService } from '../common/voidFileService.js'; @@ -65,6 +65,8 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); + + const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { const model = editor.getModel(); @@ -102,6 +104,25 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number +// finds block.orig in fileContents and return its range in file +const findTextInCode = (text: string, fileContents: string) => { + console.log('TEXTTTT', JSON.stringify(text)) + const idx = fileContents.indexOf(text) + if (idx === -1) return 'Not found' as const + const lastIdx = fileContents.lastIndexOf(text) + if (lastIdx !== idx) return 'Not unique' as const + console.log('TEXTTTT22222', JSON.stringify(fileContents.substring(0, idx))) + const startLine = fileContents.substring(0, idx).split('\n').length + const numLines = text.split('\n').length + const endLine = startLine + numLines - 1 + console.log('startline', startLine) + console.log('endline', endLine) + return [startLine, endLine] +} + + + + export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; @@ -328,6 +349,29 @@ class EditCodeService extends Disposable implements IEditCodeService { } + + + private _notifyError = (e: Parameters[0]) => { + const details = errorDetails(e.fullError) + this._notificationService.notify({ + severity: Severity.Warning, + message: `Void Error: ${e.message}`, + actions: { + secondary: [{ + id: 'void.onerror.opensettings', + enabled: true, + label: `Open Void's settings`, + tooltip: '', + class: undefined, + run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } + }] + }, + source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined + }) + } + + + // highlight the region private _addLineDecoration = (model: ITextModel | null, startLine: number, endLine: number, className: string, options?: Partial) => { if (model === null) return @@ -1000,18 +1044,11 @@ class EditCodeService extends Disposable implements IEditCodeService { // @throttle(100) - private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latestMutable: StreamLocationMutable) { + private _writeStreamedDiffZoneLLMText(uri: URI, originalCode: string, llmTextSoFar: string, deltaText: string, latestMutable: StreamLocationMutable) { // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out - const uri = diffZone._URI - const computedDiffs = findDiffs(diffZone.originalCode, llmText) - - // should always be in streaming state here - if (!diffZone._streamState.isStreaming) { - console.error('DiffZone was not in streaming state on _writeDiffZoneLLMText') - return - } + const computedDiffs = findDiffs(originalCode, llmTextSoFar) // if streaming, use diffs to figure out where to write new code // these are two different coordinate systems - new and old line number @@ -1036,8 +1073,6 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) } - - // at the start, add a newline between the stream and originalCode to make reasoning easier if (!latestMutable.addedSplitYet) { this._writeText(uri, '\n', @@ -1066,18 +1101,14 @@ class EditCodeService extends Disposable implements IEditCodeService { ) } else if (latestMutable.originalCodeStartLine > originalCodeStartLine) { - this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'), + this._writeText(uri, '\n' + originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'), { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) } latestMutable.originalCodeStartLine = originalCodeStartLine - // add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea) - diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine - - return computedDiffs - + return newCodeEndLine } @@ -1145,7 +1176,7 @@ class EditCodeService extends Disposable implements IEditCodeService { public startApplying(opts: StartApplyingOpts) { if (opts.type === 'rewrite') { - const addedDiffZone = this._initializeRewriteStream(opts) + const addedDiffZone = this._initializeWriteoverStream(opts) return addedDiffZone?.diffareaid } @@ -1183,41 +1214,55 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = uri_ // generate search/replace block text - const origFileContents = await this._voidFileService.readFile(uri) - if (origFileContents === null) return + const originalFileCode = await this._voidFileService.readFile(uri) + if (originalFileCode === null) return + const numLines = this._getNumLines(uri) + if (numLines === null) return - // // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - // this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - const userMessageContent = searchReplace_userMessage({ originalCode: origFileContents, applyStr: applyStr }) + const startLine = 1 + const endLine = numLines + + const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) const messages: LLMChatMessage[] = [ { role: 'system', content: searchReplace_systemMessage }, { role: 'user', content: userMessageContent }, ] + let streamRequestIdRef: { current: string | null } = { current: null } - const diffareaidOfBlockNum: number[] = [] - const diffAreaOriginalLines: [number, number][] = [] + let { onFinishEdit } = this._addToHistory(uri) + + // TODO replace these with whatever block we're on initially if already started + const infoOfBlockNum: { + originalLines: [number, number], // 1-indexed + finalStartLine: number, // 1-indexed + originalCode: string, + }[] = [] - // TODO replace all these with whatever block we're on initially if already started - let latestStreamLocationMutable: StreamLocationMutable | null = null - let currStreamingBlockNum = 0 let oldBlocks: ExtractedSearchReplaceBlock[] = [] - // find block.orig in fileContents and return its range in file - const findTextInCode = (text: string, fileContents: string) => { - const idx = fileContents.indexOf(text) - if (idx === -1) return 'Not found' as const - const lastIdx = fileContents.lastIndexOf(text) - if (lastIdx !== idx) return 'Not unique' as const - const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = text.split('\n').length - const endLine = startLine + numLines - 1 - return [startLine, endLine] + const adding: Omit = { + type: 'DiffZone', + originalCode: originalFileCode, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) - let { onFinishEdit } = this._addToHistory(uri) const revertAndContinueHistory = () => { this._undoHistory(uri) @@ -1225,90 +1270,55 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinishEdit = onFinishEdit_ } - const onDone = (errorMessage: false | string) => { - for (const blockNum in diffareaidOfBlockNum) { - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue - diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - } - this._refreshStylesAndDiffsInURI(uri) - if (errorMessage) { - this._notificationService.info(`Void had an error when running Apply: ${errorMessage}.\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) this error.`) - this._metricsService.capture('Error - Apply', { errorMessage }) - this._undoHistory(uri) - } - onFinishEdit() - } - - const onNewBlockStart = (blockNum: number, block: ExtractedSearchReplaceBlock): { errorStartingBlock?: undefined } | { errorStartingBlock: string } => { - console.log('STARTING BLOCK', JSON.stringify(block, null, 2)) - - - const foundInCode = findTextInCode(block.orig, origFileContents) + const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalLines: [number, number], currentLines: [number, number] } => { + const foundInCode = findTextInCode(block.orig, originalFileCode) if (typeof foundInCode === 'string') { console.log('Apply error:', foundInCode, '; trying again.') - return { errorStartingBlock: foundInCode } + return foundInCode } const [originalStart, originalEnd] = foundInCode + // compute line offset if there were changes in the past let lineOffset = 0 - // compute line offset given multiple changes for (let i = 0; i < blockNum; i += 1) { - const [diffAreaOriginalStart, diffAreaOriginalEnd] = diffAreaOriginalLines[i] - console.log('ROIGGINAL!!!', diffAreaOriginalStart, diffAreaOriginalEnd) - if (diffAreaOriginalStart > originalEnd) continue + const { originalLines: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfBlockNum[i] + const finalCode = block.final - const diffareaid = diffareaidOfBlockNum[i] - const diffArea = this.diffAreaOfId[diffareaid] + if (otherBlockOriginalStart > originalEnd) continue + if (finalCode === null) continue - - const numNewLines = diffArea.endLine - diffArea.startLine - const numOldLines = diffAreaOriginalEnd - diffAreaOriginalStart - console.log('NUM NEW', numNewLines, numOldLines) + const numNewLines = finalCode.split('\n').length + const numOldLines = otherBlockOriginalEnd - otherBlockOriginalStart + 1 lineOffset += numNewLines - numOldLines } - const startLine = originalStart + lineOffset - const endLine = originalEnd + lineOffset - console.log('adding to', startLine, endLine) - - const adding: Omit = { - type: 'DiffZone', - originalCode: block.orig, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), + return { + originalLines: [originalStart, originalEnd], + currentLines: [originalStart + lineOffset, originalEnd + lineOffset], } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) - - diffareaidOfBlockNum.push(diffZone.diffareaid) - diffAreaOriginalLines.push([originalStart, originalEnd]) - - latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - return { errorStartingBlock: undefined } } + let latestStreamLocationMutable: StreamLocationMutable | null = null + const onDone = () => { + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } + + // refresh now in case onText takes a while to get 1st message + this._refreshStylesAndDiffsInURI(uri) let shouldSendAnotherMessage = true let nMessagesSent = 0 // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it + let currStreamingBlockNum = 0 while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false nMessagesSent += 1 @@ -1324,49 +1334,73 @@ class EditCodeService extends Disposable implements IEditCodeService { for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] - if (block.state === 'done') - currStreamingBlockNum = blockNum + // if a block is done, finish it + if (block.state === 'done') { + console.log('FINISHING BLOCK') - if (block.state === 'writingOriginal') // must be done writing original + const { finalStartLine } = infoOfBlockNum[blockNum] + const numLines = block.final.split('\n').length + + this._writeText(uri, block.final, + { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalStartLine + numLines, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + currStreamingBlockNum = blockNum + 1 + } + + // must be done writing original to stream code + if (block.state === 'writingOriginal') continue - // if this is the first time we're seeing this block, add it as a diffarea - if (!(blockNum in diffareaidOfBlockNum)) { - console.log('FULLTEXT!!!!!\n', fullText) - const { errorStartingBlock } = onNewBlockStart(blockNum, block) + // if this is the first time we're seeing this block, add it as a blocknum + if (!(blockNum in infoOfBlockNum)) { + console.log('----FULLTEXT!!!!!----\n', blockNum, fullText) - if (errorStartingBlock) { + const pos = findTextInCodeWithAdjustedOffset(blockNum, block) + console.log('OFFSET', pos) + + if (typeof pos === 'string') { + const errorStartingBlock = pos console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock) - const errMsgForLLM = errorStartingBlock === 'Not found' ? 'I interrupted you because the latest ORIGINAL code could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file.' : errorStartingBlock === 'Not unique' ? 'I interrupted you because the latest ORIGINAL code shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file.' : '' - messages.push( { role: 'assistant', content: fullText }, // latest output { role: 'user', content: errMsgForLLM } // user explanation of what's wrong ) if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) - shouldSendAnotherMessage = true revertAndContinueHistory() - return + continue } + infoOfBlockNum.push({ + originalLines: pos.originalLines, + finalStartLine: pos.currentLines[0], + originalCode: block.orig, + }) + + latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + } const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) oldBlocks = blocks - // write new text to diffarea - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue - if (!latestStreamLocationMutable) continue - this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) + + // should always be in streaming state here + if (!diffZone._streamState.isStreaming) { + console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') + continue + } + + const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, diffZone.originalCode, block.final, deltaFinalText, latestStreamLocationMutable) + diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + } // end for this._refreshStylesAndDiffsInURI(uri) @@ -1379,25 +1413,39 @@ class EditCodeService extends Disposable implements IEditCodeService { const blocks = extractSearchReplaceBlocks(fullText) if (blocks.length === 0) { - this._notificationService.info(`Void: When running Apply, your model didn't output any changes we recognized. You might need to use a smarter model for Apply.`) + this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) } - for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue + // writeover the whole file + let newCode = originalFileCode + for (let blockNum = infoOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + const { originalLines } = infoOfBlockNum[blockNum] + const finalCode = blocks[blockNum].final - this._writeText(uri, block.final, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + if (finalCode === null) continue + + const [originalStart, originalEnd] = originalLines + const lines = newCode.split('\n') + newCode = [ + ...lines.slice(0, (originalStart - 1)), + ...finalCode.split('\n'), + ...lines.slice((originalEnd - 1) + 1, Infinity) + ].join('\n') + } + const numLines = this._getNumLines(uri) + if (numLines !== null) { + this._writeText(uri, newCode, + { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, { shouldRealignDiffAreas: true } ) } - onDone(false) + + onDone() }, onError: (e) => { - console.log('ERROR in SearchReplace:', e.message) - onDone(e.message) + this._notifyError(e) + onDone() + this._undoHistory(uri) }, }) @@ -1409,7 +1457,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _initializeRewriteStream(opts: StartApplyingOpts): DiffZone | undefined { + private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { const { from } = opts @@ -1516,7 +1564,7 @@ class EditCodeService extends Disposable implements IEditCodeService { else { throw new Error(`featureName ${from} is invalid`) } - const onDone = (hadError: boolean) => { + const onDone = () => { diffZone._streamState = { isStreaming: false, } this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) @@ -1528,11 +1576,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._refreshStylesAndDiffsInURI(uri) onFinishEdit() - - // if had error, revert! - if (hadError) { - this._undoHistory(diffZone._URI) - } } // refresh now in case onText takes a while to get 1st message @@ -1566,7 +1609,9 @@ class EditCodeService extends Disposable implements IEditCodeService { fullText += prevIgnoredSuffix + newText // full text, including ```, etc const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) - this._writeStreamedDiffZoneLLMText(diffZone, croppedText, deltaCroppedText, latestStreamInfoMutable) + const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) + diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + this._refreshStylesAndDiffsInURI(uri) prevIgnoredSuffix = croppedSuffix @@ -1579,26 +1624,12 @@ class EditCodeService extends Disposable implements IEditCodeService { { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) - onDone(false) + onDone() }, onError: (e) => { - const details = errorDetails(e.fullError) - this._notificationService.notify({ - severity: Severity.Warning, - message: `Void Error: ${e.message}`, - actions: { - secondary: [{ - id: 'void.onerror.opensettings', - enabled: true, - label: `Open Void's settings`, - tooltip: '', - class: undefined, - run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } - }] - }, - source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined - }) - onDone(true) + this._notifyError(e) + onDone() + this._undoHistory(uri) }, }) From 7711c74ef016017704805e457bd03fdb6dbc2288 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 01:21:56 -0800 Subject: [PATCH 57/92] reorder --- .../contrib/void/browser/editCodeService.ts | 373 +++++++++--------- 1 file changed, 188 insertions(+), 185 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index e142e519..e16ef078 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1207,6 +1207,194 @@ class EditCodeService extends Disposable implements IEditCodeService { } + + + + + private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { + + const { from } = opts + + let startLine: number + let endLine: number + let uri: URI + + if (from === 'ClickApply') { + + const uri_ = this._getActiveEditorURI() + if (!uri_) return + uri = uri_ + + // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + + // in ctrl+L the start and end lines are the full document + const numLines = this._getNumLines(uri) + if (numLines === null) return + startLine = 1 + endLine = numLines + + } + else if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone + uri = _URI + startLine = startLine_ + endLine = endLine_ + } + else { + throw new Error(`Void: diff.type not recognized on: ${from}`) + } + + const currentFileStr = this._readURI(uri) + if (currentFileStr === null) return + const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + + + let streamRequestIdRef: { current: string | null } = { current: null } + + + // add to history + const { onFinishEdit } = this._addToHistory(uri) + + // __TODO__ let users customize modelFimTags + const quickEditFIMTags = defaultQuickEditFimTags + + const adding: Omit = { + type: 'DiffZone', + originalCode, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), + } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) + + if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + } + + // now handle messages + let messages: LLMChatMessage[] + + if (from === 'ClickApply') { + const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + messages = [ + { role: 'system', content: rewriteCode_systemMessage, }, + { role: 'user', content: userContent, } + ] + } + else if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + const { _mountInfo } = ctrlKZone + const instructions = _mountInfo?.textAreaRef.current?.value ?? '' + + const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) + const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) + // type: 'messages', + messages = [ + { role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, + { role: 'user', content: userContent, } + ] + } + else { throw new Error(`featureName ${from} is invalid`) } + + + const onDone = () => { + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + + if (from === 'QuickEdit') { + const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone + + ctrlKZone._linkedStreamingDiffZone = null + this._deleteCtrlKZone(ctrlKZone) + } + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } + + // refresh now in case onText takes a while to get 1st message + this._refreshStylesAndDiffsInURI(uri) + + + const extractText = (fullText: string, recentlyAddedTextLen: number) => { + if (from === 'QuickEdit') { + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) + } + else if (from === 'ClickApply') { + return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) + } + throw 1 + } + + const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + + // state used in onText: + let fullText = '' + let prevIgnoredSuffix = '' + + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K', + logging: { loggingName: `startApplying - ${from}` }, + messages, + onText: ({ newText: newText_ }) => { + + const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! + fullText += prevIgnoredSuffix + newText // full text, including ```, etc + + const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) + const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) + diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + + this._refreshStylesAndDiffsInURI(uri) + + prevIgnoredSuffix = croppedSuffix + }, + onFinalMessage: ({ fullText }) => { + // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) + // at the end, re-write whole thing to make sure no sync errors + const [croppedText, _1, _2] = extractText(fullText, 0) + this._writeText(uri, croppedText, + { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + onDone() + }, + onError: (e) => { + this._notifyError(e) + onDone() + this._undoHistory(uri) + }, + + }) + + return diffZone + + } + + + + private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { const uri_ = this._getActiveEditorURI() @@ -1456,191 +1644,6 @@ class EditCodeService extends Disposable implements IEditCodeService { - - private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - - const { from } = opts - - let startLine: number - let endLine: number - let uri: URI - - if (from === 'ClickApply') { - - const uri_ = this._getActiveEditorURI() - if (!uri_) return - uri = uri_ - - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - - // in ctrl+L the start and end lines are the full document - const numLines = this._getNumLines(uri) - if (numLines === null) return - startLine = 1 - endLine = numLines - - } - else if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - - const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone - uri = _URI - startLine = startLine_ - endLine = endLine_ - } - else { - throw new Error(`Void: diff.type not recognized on: ${from}`) - } - - const currentFileStr = this._readURI(uri) - if (currentFileStr === null) return - const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - - - let streamRequestIdRef: { current: string | null } = { current: null } - - - // add to history - const { onFinishEdit } = this._addToHistory(uri) - - // __TODO__ let users customize modelFimTags - const quickEditFIMTags = defaultQuickEditFimTags - - const adding: Omit = { - type: 'DiffZone', - originalCode, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), - } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) - - if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - - ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid - } - - // now handle messages - let messages: LLMChatMessage[] - - if (from === 'ClickApply') { - const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, uri }) - messages = [ - { role: 'system', content: rewriteCode_systemMessage, }, - { role: 'user', content: userContent, } - ] - } - else if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - const { _mountInfo } = ctrlKZone - const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - - const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) - // type: 'messages', - messages = [ - { role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, - { role: 'user', content: userContent, } - ] - } - else { throw new Error(`featureName ${from} is invalid`) } - - - const onDone = () => { - diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - - if (from === 'QuickEdit') { - const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone - - ctrlKZone._linkedStreamingDiffZone = null - this._deleteCtrlKZone(ctrlKZone) - } - this._refreshStylesAndDiffsInURI(uri) - onFinishEdit() - } - - // refresh now in case onText takes a while to get 1st message - this._refreshStylesAndDiffsInURI(uri) - - - const extractText = (fullText: string, recentlyAddedTextLen: number) => { - if (from === 'QuickEdit') { - return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) - } - else if (from === 'ClickApply') { - return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) - } - throw 1 - } - - const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - - // state used in onText: - let fullText = '' - let prevIgnoredSuffix = '' - - streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K', - logging: { loggingName: `startApplying - ${from}` }, - messages, - onText: ({ newText: newText_ }) => { - - const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! - fullText += prevIgnoredSuffix + newText // full text, including ```, etc - - const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) - const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) - diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file - - this._refreshStylesAndDiffsInURI(uri) - - prevIgnoredSuffix = croppedSuffix - }, - onFinalMessage: ({ fullText }) => { - // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) - // at the end, re-write whole thing to make sure no sync errors - const [croppedText, _1, _2] = extractText(fullText, 0) - this._writeText(uri, croppedText, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - onDone() - }, - onError: (e) => { - this._notifyError(e) - onDone() - this._undoHistory(uri) - }, - - }) - - return diffZone - - } - - - - private _stopIfStreaming(diffZone: DiffZone) { const uri = diffZone._URI From 970f0bdb9d2e725d5efe168d3317d854028f409c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 01:28:46 -0800 Subject: [PATCH 58/92] revert "Void's" on the button --- src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 8c0ebe8b..4dce7e6c 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -270,7 +270,7 @@ export class EditorGroupWatermark extends Disposable { const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings'); const button3 = append(recentsBox, $('button')); - button3.textContent = `Void's Settings` + button3.textContent = `Void Settings` button3.style.display = 'block' button3.style.marginLeft = 'auto' button3.style.marginRight = 'auto' From 33d80bed809e7651ec3328a39caf8175e12e171f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 17:25:53 -0800 Subject: [PATCH 59/92] progress --- .../contrib/void/browser/editCodeService.ts | 198 ++++++++++-------- 1 file changed, 115 insertions(+), 83 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index e16ef078..de920242 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -106,17 +106,13 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number // finds block.orig in fileContents and return its range in file const findTextInCode = (text: string, fileContents: string) => { - console.log('TEXTTTT', JSON.stringify(text)) const idx = fileContents.indexOf(text) if (idx === -1) return 'Not found' as const const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const - console.log('TEXTTTT22222', JSON.stringify(fileContents.substring(0, idx))) const startLine = fileContents.substring(0, idx).split('\n').length const numLines = text.split('\n').length const endLine = startLine + numLines - 1 - console.log('startline', startLine) - console.log('endline', endLine) return [startLine, endLine] } @@ -1046,29 +1042,31 @@ class EditCodeService extends Disposable implements IEditCodeService { // @throttle(100) private _writeStreamedDiffZoneLLMText(uri: URI, originalCode: string, llmTextSoFar: string, deltaText: string, latestMutable: StreamLocationMutable) { + let numNewLines = 0 + // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out const computedDiffs = findDiffs(originalCode, llmTextSoFar) // if streaming, use diffs to figure out where to write new code // these are two different coordinate systems - new and old line number - let newCodeEndLine: number // get file[diffArea.startLine...newFileEndLine] with line=newFileEndLine highlighted - let originalCodeStartLine: number // get original[oldStartingPoint...] (line in the original code, so starts at 1) + let endLineInLlmTextSoFar: number // get file[diffArea.startLine...newFileEndLine] with line=newFileEndLine highlighted + let startLineInOriginalCode: number // get original[oldStartingPoint...] (line in the original code, so starts at 1) const lastDiff = computedDiffs.pop() if (!lastDiff) { // console.log('!lastDiff') // if the writing is identical so far, display no changes - originalCodeStartLine = 1 - newCodeEndLine = 1 + startLineInOriginalCode = 1 + endLineInLlmTextSoFar = 1 } else { - originalCodeStartLine = lastDiff.originalStartLine + startLineInOriginalCode = lastDiff.originalStartLine if (lastDiff.type === 'insertion' || lastDiff.type === 'edit') - newCodeEndLine = lastDiff.endLine + endLineInLlmTextSoFar = lastDiff.endLine else if (lastDiff.type === 'deletion') - newCodeEndLine = lastDiff.startLine + endLineInLlmTextSoFar = lastDiff.startLine else throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) } @@ -1080,6 +1078,7 @@ class EditCodeService extends Disposable implements IEditCodeService { { shouldRealignDiffAreas: true } ) latestMutable.addedSplitYet = true + numNewLines += 1 } // insert deltaText at latest line and col @@ -1087,28 +1086,33 @@ class EditCodeService extends Disposable implements IEditCodeService { { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) - latestMutable.line += deltaText.split('\n').length - 1 + const deltaNumNewLines = deltaText.split('\n').length - 1 + latestMutable.line += deltaNumNewLines const lastNewlineIdx = deltaText.lastIndexOf('\n') latestMutable.col = lastNewlineIdx === -1 ? latestMutable.col + deltaText.length : deltaText.length - lastNewlineIdx + numNewLines += deltaNumNewLines // delete or insert to get original up to speed - if (latestMutable.originalCodeStartLine < originalCodeStartLine) { + if (latestMutable.originalCodeStartLine < startLineInOriginalCode) { // moved up, delete - const numLinesDeleted = originalCodeStartLine - latestMutable.originalCodeStartLine + const numLinesDeleted = startLineInOriginalCode - latestMutable.originalCodeStartLine this._writeText(uri, '', { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, { shouldRealignDiffAreas: true } ) + numNewLines -= numLinesDeleted } - else if (latestMutable.originalCodeStartLine > originalCodeStartLine) { - this._writeText(uri, '\n' + originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'), + else if (latestMutable.originalCodeStartLine > startLineInOriginalCode) { + const newText = '\n' + originalCode.split('\n').slice((startLineInOriginalCode - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n') + this._writeText(uri, newText, { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) + numNewLines += newText.split('\n').length - 1 } - latestMutable.originalCodeStartLine = originalCodeStartLine + latestMutable.originalCodeStartLine = startLineInOriginalCode - return newCodeEndLine + return { endLineInLlmTextSoFar, numNewLines } } @@ -1363,8 +1367,8 @@ class EditCodeService extends Disposable implements IEditCodeService { fullText += prevIgnoredSuffix + newText // full text, including ```, etc const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) - const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) - diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + const { endLineInLlmTextSoFar } = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) + diffZone._streamState.line = (diffZone.startLine - 1) + endLineInLlmTextSoFar // change coordinate systems from originalCode to full file this._refreshStylesAndDiffsInURI(uri) @@ -1425,9 +1429,9 @@ class EditCodeService extends Disposable implements IEditCodeService { let { onFinishEdit } = this._addToHistory(uri) // TODO replace these with whatever block we're on initially if already started - const infoOfBlockNum: { - originalLines: [number, number], // 1-indexed - finalStartLine: number, // 1-indexed + const infoOfAddedBlockNum: { + originalBounds: [number, number], // 1-indexed + currentBounds: [number, number], // 1-indexed originalCode: string, }[] = [] @@ -1459,7 +1463,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalLines: [number, number], currentLines: [number, number] } => { + const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalBounds: [number, number], currentBounds: [number, number] } => { const foundInCode = findTextInCode(block.orig, originalFileCode) if (typeof foundInCode === 'string') { console.log('Apply error:', foundInCode, '; trying again.') @@ -1470,7 +1474,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // compute line offset if there were changes in the past let lineOffset = 0 for (let i = 0; i < blockNum; i += 1) { - const { originalLines: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfBlockNum[i] + const { originalBounds: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfAddedBlockNum[i] const finalCode = block.final if (otherBlockOriginalStart > originalEnd) continue @@ -1483,8 +1487,8 @@ class EditCodeService extends Disposable implements IEditCodeService { } return { - originalLines: [originalStart, originalEnd], - currentLines: [originalStart + lineOffset, originalEnd + lineOffset], + originalBounds: [originalStart, originalEnd], + currentBounds: [originalStart + lineOffset, originalEnd + lineOffset], } } @@ -1503,6 +1507,9 @@ class EditCodeService extends Disposable implements IEditCodeService { // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) + + let shouldUpdateOrigStreamPos = true + let shouldSendAnotherMessage = true let nMessagesSent = 0 // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it @@ -1518,34 +1525,31 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, onText: ({ fullText }) => { const blocks = extractSearchReplaceBlocks(fullText) + if (blocks.length === 2) return for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] - // if a block is done, finish it - if (block.state === 'done') { - console.log('FINISHING BLOCK') - - const { finalStartLine } = infoOfBlockNum[blockNum] - const numLines = block.final.split('\n').length - - this._writeText(uri, block.final, - { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalStartLine + numLines, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - currStreamingBlockNum = blockNum + 1 - } - // must be done writing original to stream code - if (block.state === 'writingOriginal') + if (block.state === 'writingOriginal') { + // update stream state + if (shouldUpdateOrigStreamPos && block.orig.trim().length >= 20) { + console.log('or') + const startingAtLine = latestStreamLocationMutable?.line ?? 1 // dont go backwards if already have a stream line + const blockOrigLines = block.orig.split('\n') + const idx = originalFileCode.indexOf(blockOrigLines.slice(startingAtLine, Infinity).join('\n'), (startingAtLine - 1)) + diffZone._streamState.line = originalFileCode.substring(0, idx).split('\n').length + shouldUpdateOrigStreamPos = false + } continue + } + shouldUpdateOrigStreamPos = true + // if this is the first time we're seeing this block, add it as a blocknum - if (!(blockNum in infoOfBlockNum)) { - console.log('----FULLTEXT!!!!!----\n', blockNum, fullText) + if (!(blockNum in infoOfAddedBlockNum)) { const pos = findTextInCodeWithAdjustedOffset(blockNum, block) - console.log('OFFSET', pos) if (typeof pos === 'string') { const errorStartingBlock = pos @@ -1565,70 +1569,98 @@ class EditCodeService extends Disposable implements IEditCodeService { continue } - infoOfBlockNum.push({ - originalLines: pos.originalLines, - finalStartLine: pos.currentLines[0], + console.log('orig bounds', pos.originalBounds) + console.log('curr bounds', pos.currentBounds) + + infoOfAddedBlockNum.push({ + originalBounds: [...pos.originalBounds], + currentBounds: [...pos.currentBounds], originalCode: block.orig, }) - latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + latestStreamLocationMutable = { line: pos.currentBounds[0], addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + + console.log('latestStreamLocation', latestStreamLocationMutable) } - const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) - oldBlocks = blocks - if (!latestStreamLocationMutable) continue + // if a block is done, finish it + if (block.state === 'done') { + const { currentBounds: [finalStartLine, finalEndLine] } = infoOfAddedBlockNum[blockNum] + console.log('FINISHING!!!!!!', blockNum, ':', finalStartLine, finalEndLine, block.final) + + + this._writeText(uri, block.final, + { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + infoOfAddedBlockNum[blockNum].currentBounds[1] = infoOfAddedBlockNum[blockNum].currentBounds[0] + block.final.split('\n').length - 1 + + currStreamingBlockNum = blockNum + 1 + } // should always be in streaming state here if (!diffZone._streamState.isStreaming) { console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') continue } + if (!latestStreamLocationMutable) continue - const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, diffZone.originalCode, block.final, deltaFinalText, latestStreamLocationMutable) - diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + + const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) + oldBlocks = blocks + + const { numNewLines } = this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) + const currLine = infoOfAddedBlockNum[blockNum].currentBounds[1] + numNewLines + console.log('currline A', currLine) + infoOfAddedBlockNum[blockNum].currentBounds[1] = currLine + diffZone._streamState.line = currLine + + console.log('delta', deltaFinalText) + console.log('currLines', infoOfAddedBlockNum[blockNum].currentBounds) } // end for + console.log('diffZone._streamState.line', diffZone._streamState.line) this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { - console.log('final message!!', fullText) + // console.log('final message!!', fullText) - // 1. wait 500ms and fix lint errors - call lint error workflow - // (update react state to say "Fixing errors") - const blocks = extractSearchReplaceBlocks(fullText) + // // 1. wait 500ms and fix lint errors - call lint error workflow + // // (update react state to say "Fixing errors") + // const blocks = extractSearchReplaceBlocks(fullText) - if (blocks.length === 0) { - this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) - } + // if (blocks.length === 0) { + // this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) + // } - // writeover the whole file - let newCode = originalFileCode - for (let blockNum = infoOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - const { originalLines } = infoOfBlockNum[blockNum] - const finalCode = blocks[blockNum].final + // // writeover the whole file + // let newCode = originalFileCode + // for (let blockNum = infoOfAddedBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + // const { originalBounds } = infoOfAddedBlockNum[blockNum] + // const finalCode = blocks[blockNum].final - if (finalCode === null) continue + // if (finalCode === null) continue - const [originalStart, originalEnd] = originalLines - const lines = newCode.split('\n') - newCode = [ - ...lines.slice(0, (originalStart - 1)), - ...finalCode.split('\n'), - ...lines.slice((originalEnd - 1) + 1, Infinity) - ].join('\n') - } - const numLines = this._getNumLines(uri) - if (numLines !== null) { - this._writeText(uri, newCode, - { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, - { shouldRealignDiffAreas: true } - ) - } + // const [originalStart, originalEnd] = originalBounds + // const lines = newCode.split('\n') + // newCode = [ + // ...lines.slice(0, (originalStart - 1)), + // ...finalCode.split('\n'), + // ...lines.slice((originalEnd - 1) + 1, Infinity) + // ].join('\n') + // } + // const numLines = this._getNumLines(uri) + // if (numLines !== null) { + // this._writeText(uri, newCode, + // { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, + // { shouldRealignDiffAreas: true } + // ) + // } - onDone() + // onDone() }, onError: (e) => { this._notifyError(e) From 1959c53d755b76fb5ec93196d833bd7270aa9154 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 23:23:55 -0800 Subject: [PATCH 60/92] progress --- .../contrib/void/browser/chatThreadService.ts | 6 ++ .../contrib/void/browser/editCodeService.ts | 59 +++++++++---------- .../contrib/void/common/toolsService.ts | 2 +- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 05ef405b..1526406d 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -156,6 +156,8 @@ export interface IChatThreadService { openNewThread(): void; switchToThread(threadId: string): void; + // you can edit multiple messages + // the one you're currently editing is "focused", and we add items to that one when you press cmd+L. getFocusedMessageIdx(): number | undefined; isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; @@ -164,8 +166,12 @@ export interface IChatThreadService { _useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial) => void]; _useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial) => void]; + // call to edit a message editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; + + // call to add a message addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise; + cancelStreaming(threadId: string): void; dismissStreamError(threadId: string): void; diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index de920242..f0ee5c45 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1619,48 +1619,47 @@ class EditCodeService extends Disposable implements IEditCodeService { console.log('delta', deltaFinalText) console.log('currLines', infoOfAddedBlockNum[blockNum].currentBounds) - } // end for console.log('diffZone._streamState.line', diffZone._streamState.line) this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { - // console.log('final message!!', fullText) + console.log('final message!!', fullText) - // // 1. wait 500ms and fix lint errors - call lint error workflow - // // (update react state to say "Fixing errors") - // const blocks = extractSearchReplaceBlocks(fullText) + // 1. wait 500ms and fix lint errors - call lint error workflow + // (update react state to say "Fixing errors") + const blocks = extractSearchReplaceBlocks(fullText) - // if (blocks.length === 0) { - // this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) - // } + if (blocks.length === 0) { + this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) + } - // // writeover the whole file - // let newCode = originalFileCode - // for (let blockNum = infoOfAddedBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - // const { originalBounds } = infoOfAddedBlockNum[blockNum] - // const finalCode = blocks[blockNum].final + // writeover the whole file + let newCode = originalFileCode + for (let blockNum = infoOfAddedBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + const { originalBounds } = infoOfAddedBlockNum[blockNum] + const finalCode = blocks[blockNum].final - // if (finalCode === null) continue + if (finalCode === null) continue - // const [originalStart, originalEnd] = originalBounds - // const lines = newCode.split('\n') - // newCode = [ - // ...lines.slice(0, (originalStart - 1)), - // ...finalCode.split('\n'), - // ...lines.slice((originalEnd - 1) + 1, Infinity) - // ].join('\n') - // } - // const numLines = this._getNumLines(uri) - // if (numLines !== null) { - // this._writeText(uri, newCode, - // { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, - // { shouldRealignDiffAreas: true } - // ) - // } + const [originalStart, originalEnd] = originalBounds + const lines = newCode.split('\n') + newCode = [ + ...lines.slice(0, (originalStart - 1)), + ...finalCode.split('\n'), + ...lines.slice((originalEnd - 1) + 1, Infinity) + ].join('\n') + } + const numLines = this._getNumLines(uri) + if (numLines !== null) { + this._writeText(uri, newCode, + { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, + { shouldRealignDiffAreas: true } + ) + } - // onDone() + onDone() }, onError: (e) => { this._notifyError(e) diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index dbdd0e15..9686920f 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -31,7 +31,7 @@ const paginationHelper = { export const voidTools = { read_file: { name: 'read_file', - description: 'Returns file contents of a given URI.', + description: `Returns file contents of a given URI. ${paginationHelper.desc}`, params: { uri: { type: 'string', description: undefined }, }, From 8918a144041c8de63fea2d3a587915660e23c38e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 23:23:58 -0800 Subject: [PATCH 61/92] almost there --- .../contrib/void/browser/editCodeService.ts | 142 ++++++++++-------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index f0ee5c45..c41fff7a 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -113,7 +113,7 @@ const findTextInCode = (text: string, fileContents: string) => { const startLine = fileContents.substring(0, idx).split('\n').length const numLines = text.split('\n').length const endLine = startLine + numLines - 1 - return [startLine, endLine] + return [startLine, endLine] as const } @@ -164,7 +164,6 @@ type CommonZoneProps = { _URI: URI; // typically we get the URI from model - _removeStylesFns: Set; // these don't remove diffs or this diffArea, only their styles } type CtrlKZone = { @@ -180,6 +179,7 @@ type CtrlKZone = { } _linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here + _removeStylesFns: Set // these don't remove diffs or this diffArea, only their styles } & CommonZoneProps @@ -199,12 +199,22 @@ type DiffZone = { }; editorId?: undefined; linkedStreamingDiffZone?: undefined; + _removeStylesFns: Set // these don't remove diffs or this diffArea, only their styles } & CommonZoneProps +type TrackingZone = { + type: 'TrackingZone'; + metadata: T; + originalCode?: undefined; + editorId?: undefined; + _removeStylesFns?: undefined; +} & CommonZoneProps + + // called DiffArea for historical purposes, we can rename to something like TextRegion if we want -type DiffArea = CtrlKZone | DiffZone +type DiffArea = CtrlKZone | DiffZone | TrackingZone const diffAreaSnapshotKeys = [ 'type', @@ -823,7 +833,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this.diffAreaOfId[diffareaid] = { ...snapshottedDiffArea as DiffAreaSnapshot, _URI: uri, - _removeStylesFns: new Set(), + _removeStylesFns: new Set(), _mountInfo: null, _linkedStreamingDiffZone: null, // when restoring, we will never be streaming } @@ -881,8 +891,8 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type === 'DiffZone') this._deleteDiffs(diffArea) - diffArea._removeStylesFns.forEach(removeStyles => removeStyles()) - diffArea._removeStylesFns.clear() + diffArea._removeStylesFns?.forEach(removeStyles => removeStyles()) + diffArea._removeStylesFns?.clear() } @@ -1112,7 +1122,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } latestMutable.originalCodeStartLine = startLineInOriginalCode - return { endLineInLlmTextSoFar, numNewLines } + return { endLineInLlmTextSoFar, numNewLines } // numNewLines here might not be correct.... } @@ -1429,13 +1439,13 @@ class EditCodeService extends Disposable implements IEditCodeService { let { onFinishEdit } = this._addToHistory(uri) // TODO replace these with whatever block we're on initially if already started - const infoOfAddedBlockNum: { - originalBounds: [number, number], // 1-indexed - currentBounds: [number, number], // 1-indexed - originalCode: string, - }[] = [] - let oldBlocks: ExtractedSearchReplaceBlock[] = [] + type SearchReplaceDiffAreaMetadata = { + originalBounds: [number, number], // 1-indexed + originalCode: string, + } + + const addedDiffAreaOfBlockNum: TrackingZone[] = [] const adding: Omit = { type: 'DiffZone', @@ -1463,18 +1473,21 @@ class EditCodeService extends Disposable implements IEditCodeService { } + const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalBounds: [number, number], currentBounds: [number, number] } => { + // call findInText const foundInCode = findTextInCode(block.orig, originalFileCode) + + // if error, return error as a string if (typeof foundInCode === 'string') { - console.log('Apply error:', foundInCode, '; trying again.') return foundInCode } - const [originalStart, originalEnd] = foundInCode - // compute line offset if there were changes in the past + // adjust based on the changes by computing line offset + const [originalStart, originalEnd] = foundInCode let lineOffset = 0 for (let i = 0; i < blockNum; i += 1) { - const { originalBounds: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfAddedBlockNum[i] + const { originalBounds: [otherBlockOriginalStart, otherBlockOriginalEnd], } = addedDiffAreaOfBlockNum[i].metadata const finalCode = block.final if (otherBlockOriginalStart > originalEnd) continue @@ -1493,9 +1506,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - let latestStreamLocationMutable: StreamLocationMutable | null = null - - const onDone = () => { diffZone._streamState = { isStreaming: false, } @@ -1507,12 +1517,14 @@ class EditCodeService extends Disposable implements IEditCodeService { // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) - + let latestStreamLocationMutable: StreamLocationMutable | null = null let shouldUpdateOrigStreamPos = true + let oldBlocks: ExtractedSearchReplaceBlock[] = [] + + // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it let shouldSendAnotherMessage = true let nMessagesSent = 0 - // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it let currStreamingBlockNum = 0 while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false @@ -1524,8 +1536,11 @@ class EditCodeService extends Disposable implements IEditCodeService { logging: { loggingName: `generateSearchAndReplace` }, messages, onText: ({ fullText }) => { + // blocks are [done done done ... {writingFinal|writingOriginal}] + // ^ + // currStreamingBlockNum + const blocks = extractSearchReplaceBlocks(fullText) - if (blocks.length === 2) return for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] @@ -1534,12 +1549,23 @@ class EditCodeService extends Disposable implements IEditCodeService { if (block.state === 'writingOriginal') { // update stream state if (shouldUpdateOrigStreamPos && block.orig.trim().length >= 20) { - console.log('or') - const startingAtLine = latestStreamLocationMutable?.line ?? 1 // dont go backwards if already have a stream line - const blockOrigLines = block.orig.split('\n') - const idx = originalFileCode.indexOf(blockOrigLines.slice(startingAtLine, Infinity).join('\n'), (startingAtLine - 1)) - diffZone._streamState.line = originalFileCode.substring(0, idx).split('\n').length - shouldUpdateOrigStreamPos = false + + const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line + + const originalCodeStartingAtLine = originalFileCode.split('\n').slice(startingAtLine, Infinity).join('\n') + const idxInAfter = originalCodeStartingAtLine.indexOf(block.orig) + + console.log('SEARCHED FOR:\n', block.orig, '\nIN:\n', originalCodeStartingAtLine, '\nGOT:\n', idxInAfter) + + if (idxInAfter !== -1) { + const numOriginalCodeLinesBefore = originalCodeStartingAtLine.substring(0, idxInAfter).split('\n').length + const lineNum = startingAtLine + numOriginalCodeLinesBefore + + console.log('SWITCHING TO LINE', lineNum) + + diffZone._streamState.line = lineNum + shouldUpdateOrigStreamPos = false + } } continue } @@ -1547,10 +1573,10 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a blocknum - if (!(blockNum in infoOfAddedBlockNum)) { - + if (!(blockNum in addedDiffAreaOfBlockNum)) { const pos = findTextInCodeWithAdjustedOffset(blockNum, block) + // if error if (typeof pos === 'string') { const errorStartingBlock = pos console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock) @@ -1569,34 +1595,29 @@ class EditCodeService extends Disposable implements IEditCodeService { continue } - console.log('orig bounds', pos.originalBounds) - console.log('curr bounds', pos.currentBounds) - - infoOfAddedBlockNum.push({ - originalBounds: [...pos.originalBounds], - currentBounds: [...pos.currentBounds], - originalCode: block.orig, - }) - + // otherwise if no error, add the position as a diffarea + const adding: Omit, 'diffareaid'> = { + type: 'TrackingZone', + startLine: pos.currentBounds[0], + endLine: pos.currentBounds[1], + _URI: uri, + metadata: { + originalBounds: pos.originalBounds, + originalCode: block.orig, + }, + } + const trackingZone = this._addDiffArea(adding) + addedDiffAreaOfBlockNum.push(trackingZone) latestStreamLocationMutable = { line: pos.currentBounds[0], addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - - console.log('latestStreamLocation', latestStreamLocationMutable) - - } - + } // <-- done adding diffarea // if a block is done, finish it if (block.state === 'done') { - const { currentBounds: [finalStartLine, finalEndLine] } = infoOfAddedBlockNum[blockNum] - console.log('FINISHING!!!!!!', blockNum, ':', finalStartLine, finalEndLine, block.final) - - + const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] this._writeText(uri, block.final, { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) - infoOfAddedBlockNum[blockNum].currentBounds[1] = infoOfAddedBlockNum[blockNum].currentBounds[0] + block.final.split('\n').length - 1 - currStreamingBlockNum = blockNum + 1 } @@ -1608,20 +1629,17 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!latestStreamLocationMutable) continue + // write the added text to the file + const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) + this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) + diffZone._streamState.line = currentEndLine + console.log('CURRENT LINE', currentEndLine) + oldBlocks = blocks - const { numNewLines } = this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) - const currLine = infoOfAddedBlockNum[blockNum].currentBounds[1] + numNewLines - console.log('currline A', currLine) - infoOfAddedBlockNum[blockNum].currentBounds[1] = currLine - diffZone._streamState.line = currLine - - console.log('delta', deltaFinalText) - console.log('currLines', infoOfAddedBlockNum[blockNum].currentBounds) } // end for - console.log('diffZone._streamState.line', diffZone._streamState.line) this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { @@ -1637,8 +1655,8 @@ class EditCodeService extends Disposable implements IEditCodeService { // writeover the whole file let newCode = originalFileCode - for (let blockNum = infoOfAddedBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - const { originalBounds } = infoOfAddedBlockNum[blockNum] + for (let blockNum = addedDiffAreaOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + const { originalBounds } = addedDiffAreaOfBlockNum[blockNum].metadata const finalCode = blocks[blockNum].final if (finalCode === null) continue From 73c3a8133ac431f6c092861e9c7c4b70b4f626b2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 23:59:16 -0800 Subject: [PATCH 62/92] massively simplified, seems to work! --- .../contrib/void/browser/editCodeService.ts | 94 ++++++++----------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index c41fff7a..2cf813cd 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -105,8 +105,13 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number // finds block.orig in fileContents and return its range in file -const findTextInCode = (text: string, fileContents: string) => { - const idx = fileContents.indexOf(text) +// startingAtLine is 1-indexed and inclusive +const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => { + const idx = fileContents.indexOf(text, + startingAtLine !== undefined ? + fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine + : 0 + ) if (idx === -1) return 'Not found' as const const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const @@ -1473,36 +1478,30 @@ class EditCodeService extends Disposable implements IEditCodeService { } - - const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalBounds: [number, number], currentBounds: [number, number] } => { - // call findInText - const foundInCode = findTextInCode(block.orig, originalFileCode) - - // if error, return error as a string - if (typeof foundInCode === 'string') { - return foundInCode - } - + const convertOriginalRangeToFinalRange = (originalRange: readonly [number, number]): [number, number] => { // adjust based on the changes by computing line offset - const [originalStart, originalEnd] = foundInCode + const [originalStart, originalEnd] = originalRange let lineOffset = 0 - for (let i = 0; i < blockNum; i += 1) { - const { originalBounds: [otherBlockOriginalStart, otherBlockOriginalEnd], } = addedDiffAreaOfBlockNum[i].metadata - const finalCode = block.final - - if (otherBlockOriginalStart > originalEnd) continue - if (finalCode === null) continue - - const numNewLines = finalCode.split('\n').length - const numOldLines = otherBlockOriginalEnd - otherBlockOriginalStart + 1 - + for (const blockDiffArea of addedDiffAreaOfBlockNum) { + const { + startLine, endLine, + metadata: { originalBounds: [originalStart2, originalEnd2], }, + } = blockDiffArea + if (originalStart2 >= originalEnd) continue + const numNewLines = endLine - startLine + 1 + const numOldLines = originalEnd2 - originalStart2 + 1 lineOffset += numNewLines - numOldLines } + return [originalStart + lineOffset, originalEnd + lineOffset] + } - return { - originalBounds: [originalStart, originalEnd], - currentBounds: [originalStart + lineOffset, originalEnd + lineOffset], - } + + const errMsgOfInvalidStr = (str: string & ReturnType) => { + return str === 'Not found' ? + 'I interrupted you because the latest ORIGINAL code could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file.' + : str === 'Not unique' ? + 'I interrupted you because the latest ORIGINAL code shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file.' + : '' } @@ -1549,21 +1548,11 @@ class EditCodeService extends Disposable implements IEditCodeService { if (block.state === 'writingOriginal') { // update stream state if (shouldUpdateOrigStreamPos && block.orig.trim().length >= 20) { - const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line - - const originalCodeStartingAtLine = originalFileCode.split('\n').slice(startingAtLine, Infinity).join('\n') - const idxInAfter = originalCodeStartingAtLine.indexOf(block.orig) - - console.log('SEARCHED FOR:\n', block.orig, '\nIN:\n', originalCodeStartingAtLine, '\nGOT:\n', idxInAfter) - - if (idxInAfter !== -1) { - const numOriginalCodeLinesBefore = originalCodeStartingAtLine.substring(0, idxInAfter).split('\n').length - const lineNum = startingAtLine + numOriginalCodeLinesBefore - - console.log('SWITCHING TO LINE', lineNum) - - diffZone._streamState.line = lineNum + const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine) + if (typeof originalRange !== 'string') { + const [_, endLine] = convertOriginalRangeToFinalRange(originalRange) + diffZone._streamState.line = endLine shouldUpdateOrigStreamPos = false } } @@ -1574,20 +1563,13 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a blocknum if (!(blockNum in addedDiffAreaOfBlockNum)) { - const pos = findTextInCodeWithAdjustedOffset(blockNum, block) + const originalBounds = findTextInCode(block.orig, originalFileCode) // if error - if (typeof pos === 'string') { - const errorStartingBlock = pos - console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock) - const errMsgForLLM = errorStartingBlock === 'Not found' ? - 'I interrupted you because the latest ORIGINAL code could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file.' - : errorStartingBlock === 'Not unique' ? - 'I interrupted you because the latest ORIGINAL code shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file.' - : '' + if (typeof originalBounds === 'string') { messages.push( { role: 'assistant', content: fullText }, // latest output - { role: 'user', content: errMsgForLLM } // user explanation of what's wrong + { role: 'user', content: errMsgOfInvalidStr(originalBounds) } // user explanation of what's wrong ) if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) shouldSendAnotherMessage = true @@ -1595,20 +1577,22 @@ class EditCodeService extends Disposable implements IEditCodeService { continue } + const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) + // otherwise if no error, add the position as a diffarea const adding: Omit, 'diffareaid'> = { type: 'TrackingZone', - startLine: pos.currentBounds[0], - endLine: pos.currentBounds[1], + startLine: startLine, + endLine: endLine, _URI: uri, metadata: { - originalBounds: pos.originalBounds, + originalBounds: [...originalBounds], originalCode: block.orig, }, } const trackingZone = this._addDiffArea(adding) addedDiffAreaOfBlockNum.push(trackingZone) - latestStreamLocationMutable = { line: pos.currentBounds[0], addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } // <-- done adding diffarea // if a block is done, finish it From 09f5d3e14b5bb686389dbc37f7a6cbd11474b425 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 00:19:58 -0800 Subject: [PATCH 63/92] nitpick style --- .../contrib/void/browser/editCodeService.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2cf813cd..ffae89c7 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1517,7 +1517,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._refreshStylesAndDiffsInURI(uri) let latestStreamLocationMutable: StreamLocationMutable | null = null - let shouldUpdateOrigStreamPos = true + let shouldUpdateOrigStreamStyle = true let oldBlocks: ExtractedSearchReplaceBlock[] = [] @@ -1544,24 +1544,25 @@ class EditCodeService extends Disposable implements IEditCodeService { for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] - // must be done writing original to stream code if (block.state === 'writingOriginal') { - // update stream state - if (shouldUpdateOrigStreamPos && block.orig.trim().length >= 20) { + // update stream state to the first line of original if some portion of original has been written + if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) { const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine) if (typeof originalRange !== 'string') { - const [_, endLine] = convertOriginalRangeToFinalRange(originalRange) - diffZone._streamState.line = endLine - shouldUpdateOrigStreamPos = false + const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) + diffZone._streamState.line = startLine + console.log('CURRENT LINE A', startLine) + shouldUpdateOrigStreamStyle = false } } + // must be done writing original to move on to writing streamed content continue } - shouldUpdateOrigStreamPos = true + shouldUpdateOrigStreamStyle = true - // if this is the first time we're seeing this block, add it as a blocknum + // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming if (!(blockNum in addedDiffAreaOfBlockNum)) { const originalBounds = findTextInCode(block.orig, originalFileCode) @@ -1595,7 +1596,7 @@ class EditCodeService extends Disposable implements IEditCodeService { latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } // <-- done adding diffarea - // if a block is done, finish it + // if a block is done, finish it by writing all if (block.state === 'done') { const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] this._writeText(uri, block.final, @@ -1614,14 +1615,17 @@ class EditCodeService extends Disposable implements IEditCodeService { // write the added text to the file - const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) - diffZone._streamState.line = currentEndLine - console.log('CURRENT LINE', currentEndLine) - oldBlocks = blocks + // update stream line if it's still streaming (otherwise another block might be streaming) + if (block.state !== 'done') { + const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] + diffZone._streamState.line = currentEndLine + } + + } // end for this._refreshStylesAndDiffsInURI(uri) From 06ce9e1017161eb7fca0edaad895ead605bc6723 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 15:06:42 -0800 Subject: [PATCH 64/92] settings styles + password field --- .../contrib/void/browser/editCodeService.ts | 29 ++++++++--------- .../void/browser/react/src/util/inputs.tsx | 4 ++- .../react/src/void-settings-tsx/Settings.tsx | 32 ++++++++++--------- .../contrib/void/common/toolsService.ts | 13 ++++++++ .../contrib/void/common/voidSettingsTypes.ts | 8 +++-- .../void/electron-main/llmMessage/openai.ts | 1 + 6 files changed, 53 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index ffae89c7..ad8fa505 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1596,15 +1596,6 @@ class EditCodeService extends Disposable implements IEditCodeService { latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } // <-- done adding diffarea - // if a block is done, finish it by writing all - if (block.state === 'done') { - const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] - this._writeText(uri, block.final, - { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - currStreamingBlockNum = blockNum + 1 - } // should always be in streaming state here if (!diffZone._streamState.isStreaming) { @@ -1613,17 +1604,25 @@ class EditCodeService extends Disposable implements IEditCodeService { } if (!latestStreamLocationMutable) continue + // if a block is done, finish it by writing all + if (block.state === 'done') { + const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] + this._writeText(uri, block.final, + { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + diffZone._streamState.line = finalEndLine + 1 + currStreamingBlockNum = blockNum + 1 + continue + } // write the added text to the file const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) - oldBlocks = blocks + oldBlocks = blocks // oldblocks is only used if writingFinal - // update stream line if it's still streaming (otherwise another block might be streaming) - if (block.state !== 'done') { - const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] - diffZone._streamState.line = currentEndLine - } + const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] + diffZone._streamState.line = currentEndLine } // end for 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 e62e7a9e..270870a7 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 @@ -152,12 +152,13 @@ export const VoidInputBox2 = forwardRef(fun }) -export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: { +export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, isPasswordField, multiline }: { onChangeText: (value: string) => void; styles?: Partial, onCreateInstance?: (instance: InputBox) => void | IDisposable[]; inputBoxRef?: { current: InputBox | null }; placeholder: string; + isPasswordField?: boolean; multiline: boolean; }) => { @@ -182,6 +183,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac }, placeholder, tooltip: '', + type: isPasswordField ? 'password' : undefined, flexibleHeight: multiline, flexibleMaxHeight: 500, flexibleWidth: false, diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index fd612c43..a6aec380 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -21,7 +21,7 @@ import { os } from '../../../helpers/systemInfo.js' const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => { - return
    + return
    @@ -82,9 +82,7 @@ const RefreshableModels = () => { const buttons = refreshableProviderNames.map(providerName => { if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null - return
    - -
    + return }) return <> @@ -257,7 +255,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider // const { title: providerTitle, } = displayInfoOfProviderName(providerName) - const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName) + const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName) const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') @@ -269,6 +267,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider { if (weChangedTextRef) return voidSettingsService.setSettingOfProvider(providerName, settingName, newVal) @@ -291,6 +290,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider return [disposable] }, [voidSettingsService, providerName, settingName])} multiline={false} + isPasswordField={isPasswordField} /> {subTextMd === undefined ? null :
    @@ -339,7 +339,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = {needsModel ? providerName === 'ollama' ? - : + : : null}
    @@ -368,15 +368,16 @@ export const AutoRefreshToggle = () => { // right now this is just `enabled_autoRefreshModels` const enabled = voidSettingsState.globalSettings[settingName] - return { - voidSettingsService.setGlobalSetting(settingName, !enabled) - metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) - }} - text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} - icon={enabled ? : } - disabled={false} - /> + return { + voidSettingsService.setGlobalSetting(settingName, !enabled) + metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) + }} + text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} + icon={enabled ? : } + disabled={false} + /> + } export const AIInstructionsBox = () => { @@ -400,6 +401,7 @@ export const FeaturesTab = () => { +
    diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 9686920f..522f17c5 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -68,6 +68,18 @@ export const voidTools = { required: ['query'], }, + // go_to_definition: + + // go_to_usages: + + // create_file: { + // name: 'create_file', + // description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.` + // params: { + // uri: { type: 'string', description: undefined }, + // } + // } + // semantic_search: { // description: 'Searches files semantically for the given string query.', // // RAG @@ -86,6 +98,7 @@ export type ToolCallReturnType : T extends 'list_dir' ? string : T extends 'pathname_search' ? string | URI[] : T extends 'search' ? string | URI[] + : T extends 'create_file' ? string : never export type ToolFns = { [T in ToolName]: (p: string) => Promise<[ToolCallReturnType, boolean]> } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 06e708fd..96337d33 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -505,9 +505,10 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn } type DisplayInfo = { - title: string, - placeholder: string, - subTextMd?: string, + title: string; + placeholder: string; + subTextMd?: string; + isPasswordField?: boolean; } export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => { if (settingName === 'apiKey') { @@ -537,6 +538,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : providerName === 'openAICompatible' ? undefined : '', + isPasswordField: true, } } else if (settingName === 'endpoint') { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 66c0ffe1..37b8b468 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -201,6 +201,7 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: me // message let newText = '' newText += chunk.choices[0]?.delta?.content ?? '' + console.log('!!!!', chunk.choices[0]?.delta) fullText += newText; onText({ newText, fullText }); From ce14986d2f7a5b9a56f039f65363086d9146a6d8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 16:19:01 -0800 Subject: [PATCH 65/92] split apply buttons --- .../src/markdown/ApplyBlockHoverButtons.tsx | 84 +++++++++++++++++++ .../react/src/markdown/ChatMarkdownRender.tsx | 78 +---------------- .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/common/voidSettingsTypes.ts | 2 +- .../void/electron-main/llmMessage/openai.ts | 4 +- 5 files changed, 91 insertions(+), 79 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx 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 new file mode 100644 index 00000000..c7f4b52c --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -0,0 +1,84 @@ +import { useState, useEffect, useCallback } from 'react' +import { useAccessor } from '../util/services.js' + +enum CopyButtonText { + Idle = 'Copy', + Copied = 'Copied!', + Error = 'Could not copy', +} + +const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' + +const CopyButton = ({ codeStr }: { codeStr: string }) => { + const accessor = useAccessor() + + const metricsService = accessor.get('IMetricsService') + const clipboardService = accessor.get('IClipboardService') + const [copyButtonText, setCopyButtonText] = useState(CopyButtonText.Idle) + + useEffect(() => { + if (copyButtonText === CopyButtonText.Idle) return + setTimeout(() => { + setCopyButtonText(CopyButtonText.Idle) + }, COPY_FEEDBACK_TIMEOUT) + }, [copyButtonText]) + + + const onCopy = useCallback(() => { + clipboardService.writeText(codeStr) + .then(() => { setCopyButtonText(CopyButtonText.Copied) }) + .catch(() => { setCopyButtonText(CopyButtonText.Error) }) + metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only + }, [metricsService, clipboardService, codeStr]) + + const isSingleLine = !codeStr.includes('\n') + + return +} + + + +const ApplyButton = ({ codeStr }: { codeStr: string }) => { + const accessor = useAccessor() + + const editCodeService = accessor.get('IEditCodeService') + const metricsService = accessor.get('IMetricsService') + + + const onApply = useCallback(() => { + + editCodeService.startApplying({ + from: 'ClickApply', + type: 'searchReplace', + applyStr: codeStr, + }) + metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + }, [metricsService, editCodeService, codeStr]) + + const isSingleLine = !codeStr.includes('\n') + + return + +} + + + + + +export const ApplyBlockHoverButtons = ({ codeStr }: { codeStr: string }) => { + return <> + + + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 7a9953b7..6f737ccb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -3,22 +3,12 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { JSX, useCallback, useEffect, useState } from 'react' +import React, { JSX } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' -import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js' import { ChatMessageLocation, } from '../../../aiRegexService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' - - -enum CopyButtonState { - Copy = 'Copy', - Copied = 'Copied!', - Error = 'Could not copy', -} - -const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' - +import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } @@ -29,60 +19,6 @@ const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => -const ApplyButtonsOnHover = ({ applyStr }: { applyStr: string }) => { - const accessor = useAccessor() - - const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) - const editCodeService = accessor.get('IEditCodeService') - const clipboardService = accessor.get('IClipboardService') - const metricsService = accessor.get('IMetricsService') - - useEffect(() => { - - if (copyButtonState !== CopyButtonState.Copy) { - setTimeout(() => { - setCopyButtonState(CopyButtonState.Copy) - }, COPY_FEEDBACK_TIMEOUT) - } - }, [copyButtonState]) - - const onCopy = useCallback(() => { - clipboardService.writeText(applyStr) - .then(() => { setCopyButtonState(CopyButtonState.Copied) }) - .catch(() => { setCopyButtonState(CopyButtonState.Error) }) - metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only - - }, [metricsService, clipboardService, applyStr]) - - const onApply = useCallback(() => { - - editCodeService.startApplying({ - from: 'ClickApply', - type: 'searchReplace', - applyStr, - }) - metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only - }, [metricsService, editCodeService, applyStr]) - - const isSingleLine = !applyStr.includes('\n') - - return <> - - - -} - export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => { return } + buttonsOnHover={} /> } 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 e9169280..30351697 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 @@ -703,7 +703,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM className={` relative ${mode === 'edit' ? 'px-2 w-full max-w-full' - : role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre + : role === 'user' ? `my-0.5 px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre : role === 'assistant' ? `px-2 self-start w-full max-w-full` : '' } `} diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 96337d33..529a872c 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -545,7 +545,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName return { title: providerName === 'ollama' ? 'Endpoint' : providerName === 'vLLM' ? 'Endpoint' : - providerName === 'openAICompatible' ? 'baseURL' :// (do not include /chat/completions) + providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions) '(never)', placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 37b8b468..7769a983 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -10,12 +10,12 @@ import { InternalToolInfo } from '../../common/toolsService.js'; import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; import { isAToolName } from './postprocessToolCalls.js'; -// import { parseMaxTokensStr } from './util.js'; // developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command // prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting +// npm i @openrouter/ai-sdk-provider ai ollama-ai-provider export const toOpenAITool = (toolInfo: InternalToolInfo) => { const { name, description, params, required } = toolInfo @@ -201,7 +201,7 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: me // message let newText = '' newText += chunk.choices[0]?.delta?.content ?? '' - console.log('!!!!', chunk.choices[0]?.delta) + console.log('!!!!', JSON.stringify(chunk, null, 2)) fullText += newText; onText({ newText, fullText }); From b005b1e95aafeb99c26c9adc801df3c75c1da625 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 18:53:55 -0800 Subject: [PATCH 66/92] progress (BROKEN) --- .../contrib/void/browser/editCodeService.ts | 62 +++++++++++-------- .../src/markdown/ApplyBlockHoverButtons.tsx | 12 ++-- .../react/src/markdown/ChatMarkdownRender.tsx | 10 ++- .../contrib/void/common/voidFileService.ts | 6 +- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index ad8fa505..228d7a2f 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -248,7 +248,7 @@ type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean export interface IEditCodeService { readonly _serviceBrand: undefined; - startApplying(opts: StartApplyingOpts): number | undefined; + startApplying(opts: StartApplyingOpts): number | void; interruptStreaming(diffareaid: number): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; @@ -270,7 +270,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // only applies to diffZones // streamingDiffZones: Set = new Set() - private readonly _onDidChangeStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + private readonly _onDidChangeStreaming = new Emitter<{ uri: URI }>(); private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); @@ -471,7 +471,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const buttonsWidget = new AcceptAllRejectAllWidget({ editor, onAcceptAll: () => { - this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + this.removeDiffAreas({ uri, behavior: 'keep', removeCtrlKs: false }) this._metricsService.capture('Accept All', {}) }, onRejectAll: () => { @@ -918,6 +918,11 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) } + private _deleteTrackingZone(trackingZone: TrackingZone) { + delete this.diffAreaOfId[trackingZone.diffareaid] + this.diffAreasOfURI[trackingZone._URI.fsPath].delete(trackingZone.diffareaid.toString()) + } + private _deleteCtrlKZone(ctrlKZone: CtrlKZone) { this._clearAllEffects(ctrlKZone._URI) ctrlKZone._mountInfo?.dispose() @@ -1200,11 +1205,11 @@ class EditCodeService extends Disposable implements IEditCodeService { } else if (opts.type === 'searchReplace') { - this._initializeSearchAndReplaceStream(opts) - return undefined + const addedDiffZone = this._initializeSearchAndReplaceStream(opts) + return addedDiffZone?.diffareaid } - else return undefined + return undefined } @@ -1297,7 +1302,7 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) this._onDidAddOrDeleteDiffZones.fire({ uri }) if (from === 'QuickEdit') { @@ -1339,7 +1344,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) if (from === 'QuickEdit') { const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone @@ -1414,14 +1419,14 @@ class EditCodeService extends Disposable implements IEditCodeService { - private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { + private _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { const uri_ = this._getActiveEditorURI() if (!uri_) return const uri = uri_ // generate search/replace block text - const originalFileCode = await this._voidFileService.readFile(uri) + const originalFileCode = this._voidFileService.readModel(uri) if (originalFileCode === null) return const numLines = this._getNumLines(uri) @@ -1439,6 +1444,7 @@ class EditCodeService extends Disposable implements IEditCodeService { { role: 'user', content: userMessageContent }, ] + // can use this as a proxy to set the diffArea's stream state requestId let streamRequestIdRef: { current: string | null } = { current: null } let { onFinishEdit } = this._addToHistory(uri) @@ -1450,7 +1456,7 @@ class EditCodeService extends Disposable implements IEditCodeService { originalCode: string, } - const addedDiffAreaOfBlockNum: TrackingZone[] = [] + const addedTrackingZoneOfBlockNum: TrackingZone[] = [] const adding: Omit = { type: 'DiffZone', @@ -1467,7 +1473,7 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) this._onDidAddOrDeleteDiffZones.fire({ uri }) @@ -1482,7 +1488,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // adjust based on the changes by computing line offset const [originalStart, originalEnd] = originalRange let lineOffset = 0 - for (const blockDiffArea of addedDiffAreaOfBlockNum) { + for (const blockDiffArea of addedTrackingZoneOfBlockNum) { const { startLine, endLine, metadata: { originalBounds: [originalStart2, originalEnd2], }, @@ -1505,17 +1511,23 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) this._refreshStylesAndDiffsInURI(uri) + + // delete the tracking zones + for (const trackingZone of addedTrackingZoneOfBlockNum) + this._deleteTrackingZone(trackingZone) + onFinishEdit() + shouldSendAnotherMessage = false } // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) + // stream style related let latestStreamLocationMutable: StreamLocationMutable | null = null let shouldUpdateOrigStreamStyle = true @@ -1552,7 +1564,6 @@ class EditCodeService extends Disposable implements IEditCodeService { if (typeof originalRange !== 'string') { const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) diffZone._streamState.line = startLine - console.log('CURRENT LINE A', startLine) shouldUpdateOrigStreamStyle = false } } @@ -1563,7 +1574,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming - if (!(blockNum in addedDiffAreaOfBlockNum)) { + if (!(blockNum in addedTrackingZoneOfBlockNum)) { const originalBounds = findTextInCode(block.orig, originalFileCode) // if error @@ -1592,7 +1603,7 @@ class EditCodeService extends Disposable implements IEditCodeService { }, } const trackingZone = this._addDiffArea(adding) - addedDiffAreaOfBlockNum.push(trackingZone) + addedTrackingZoneOfBlockNum.push(trackingZone) latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } // <-- done adding diffarea @@ -1606,7 +1617,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // if a block is done, finish it by writing all if (block.state === 'done') { - const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] + const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum] this._writeText(uri, block.final, { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } @@ -1621,7 +1632,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) oldBlocks = blocks // oldblocks is only used if writingFinal - const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] + const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] diffZone._streamState.line = currentEndLine @@ -1642,8 +1653,8 @@ class EditCodeService extends Disposable implements IEditCodeService { // writeover the whole file let newCode = originalFileCode - for (let blockNum = addedDiffAreaOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - const { originalBounds } = addedDiffAreaOfBlockNum[blockNum].metadata + for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata const finalCode = blocks[blockNum].final if (finalCode === null) continue @@ -1676,6 +1687,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } + return diffZone } @@ -1689,7 +1701,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._llmMessageService.abort(streamRequestId) diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) } _undoHistory(uri: URI) { @@ -1737,7 +1749,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // remove a batch of diffareas all at once (and handle accept/reject of their diffs) - public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { + public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'keep' }) { const diffareaids = this.diffAreasOfURI[uri.fsPath] if (diffareaids.size === 0) return // do nothing @@ -1750,7 +1762,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type == 'DiffZone') { if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea) - else if (behavior === 'accept') this._deleteDiffZone(diffArea) + else if (behavior === 'keep') this._deleteDiffZone(diffArea) } else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) { this._deleteCtrlKZone(diffArea) 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 c7f4b52c..d279c631 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 @@ -43,7 +43,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { -const ApplyButton = ({ codeStr }: { codeStr: string }) => { +const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') @@ -51,13 +51,15 @@ const ApplyButton = ({ codeStr }: { codeStr: string }) => { const onApply = useCallback(() => { - - editCodeService.startApplying({ + const diffareaid = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, }) + metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + + }, [metricsService, editCodeService, codeStr]) const isSingleLine = !codeStr.includes('\n') @@ -76,9 +78,9 @@ const ApplyButton = ({ codeStr }: { codeStr: string }) => { -export const ApplyBlockHoverButtons = ({ codeStr }: { codeStr: string }) => { +export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string | null }) => { return <> - + {codeBoxId !== null && } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 6f737ccb..b5378de7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -13,7 +13,7 @@ import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } -const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { +const getCodeBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { return `${threadId}-${messageIdx}-${tokenIdx}` } @@ -45,10 +45,16 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati if (t.type === "code") { + const codeBoxId = chatMessageLocation ? getCodeBoxId({ + threadId: chatMessageLocation.threadId, + messageIdx: chatMessageLocation.messageIdx, + tokenIdx: tokenIdx, + }) : null + return } + buttonsOnHover={} /> } diff --git a/src/vs/workbench/contrib/void/common/voidFileService.ts b/src/vs/workbench/contrib/void/common/voidFileService.ts index 668f1869..a7c25631 100644 --- a/src/vs/workbench/contrib/void/common/voidFileService.ts +++ b/src/vs/workbench/contrib/void/common/voidFileService.ts @@ -20,7 +20,7 @@ export interface IVoidFileService { readonly _serviceBrand: undefined; readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise; - + readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null; } export const IVoidFileService = createDecorator('VoidFileService'); @@ -39,7 +39,7 @@ export class VoidFileService implements IVoidFileService { readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { // attempt to read the model - const modelResult = await this._readModel(uri, range); + const modelResult = this.readModel(uri, range); if (modelResult) return modelResult; // if no model, read the raw file @@ -71,7 +71,7 @@ export class VoidFileService implements IVoidFileService { } - _readModel = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + readModel = (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null => { // read saved model (sometimes null if the user reloads application) let model = this.modelService.getModel(uri); From 86dfc5521d1c88a6a28b091beca375ed447d19d3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 20:10:02 -0800 Subject: [PATCH 67/92] more progress (still broken) + fix weird _useThread() + rm initStreamingDiffZoneId --- .../contrib/void/browser/chatThreadService.ts | 49 ++++++------ .../contrib/void/browser/editCodeService.ts | 77 +++++++++++++------ .../contrib/void/browser/quickEditActions.ts | 1 - .../src/markdown/ApplyBlockHoverButtons.tsx | 40 +++++++++- .../src/quick-edit-tsx/QuickEditChat.tsx | 26 +++---- .../react/src/sidebar-tsx/SidebarChat.tsx | 13 ++-- .../void/browser/react/src/util/services.tsx | 15 ++++ .../contrib/void/browser/sidebarActions.ts | 10 +-- 8 files changed, 150 insertions(+), 81 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 1526406d..c325fc8e 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -162,9 +162,12 @@ export interface IChatThreadService { isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; - // _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; - _useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial) => void]; - _useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial) => void]; + // exposed getters/setters + getCurrentMessageState: (messageIdx: number) => UserMessageState + setCurrentMessageState: (messageIdx: number, newState: Partial) => void + getCurrentThreadStagingSelections: () => StagingSelectionItem[] + setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void + // call to edit a message editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; @@ -622,33 +625,27 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + getCurrentThreadStagingSelections = () => { + return this.getCurrentThread().state.stagingSelections + } + + setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => { + this._setCurrentThreadState({ stagingSelections }) + } + // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) - _useCurrentMessageState(messageIdx: number) { - - const thread = this.getCurrentThread() - const messages = thread.messages - const currMessage = messages[messageIdx] - - if (currMessage.role !== 'user') { - return [defaultMessageState, (s: any) => { }] as const - } - - const state = currMessage.state - const setState = (newState: Partial) => this._setCurrentMessageState(newState, messageIdx) - - return [state, setState] as const - + getCurrentMessageState(messageIdx: number): UserMessageState { + const currMessage = this.getCurrentThread()?.messages?.[messageIdx] + if (!currMessage || currMessage.role !== 'user') return defaultMessageState + return currMessage.state + } + setCurrentMessageState(messageIdx: number, newState: Partial) { + const currMessage = this.getCurrentThread()?.messages?.[messageIdx] + if (!currMessage || currMessage.role !== 'user') return + this._setCurrentMessageState(newState, messageIdx) } - _useCurrentThreadState() { - const thread = this.getCurrentThread() - - const state = thread.state - const setState = this._setCurrentThreadState.bind(this) - - return [state, setState] as const - } } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 228d7a2f..657584ed 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -248,10 +248,17 @@ type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean export interface IEditCodeService { readonly _serviceBrand: undefined; - startApplying(opts: StartApplyingOpts): number | void; - interruptStreaming(diffareaid: number): void; + startApplying(opts: StartApplyingOpts): URI | null; + addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; + + isDiffZoneStreaming(opts: { diffareaid: number }): boolean; + isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean; + + interruptDiffZoneStreaming(opts: { diffareaid: number }): void; + interruptCtrlKStreaming(opts: { diffareaid: number }): void; + // testDiffs(): void; } @@ -274,6 +281,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @ICodeEditorService private readonly _editorService: ICodeEditorService, @@ -309,7 +317,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // when a stream starts or ends let removeAcceptRejectAllUI: (() => void) | null = null - const onChangeUriState = () => { + const changeUriState = () => { const uri = model.uri const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] .map(diffareaid => this.diffAreaOfId[diffareaid]) @@ -322,8 +330,8 @@ class EditCodeService extends Disposable implements IEditCodeService { removeAcceptRejectAllUI = null } } - this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() })) - this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() })) + this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) + this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) } // initialize all existing models + initialize when a new model mounts for (let model of this._modelService.getModels()) { initializeModel(model) } @@ -526,7 +534,6 @@ class EditCodeService extends Disposable implements IEditCodeService { mountCtrlK(domNode, accessor, { diffareaid: ctrlKZone.diffareaid, - initStreamingDiffZoneId: ctrlKZone._linkedStreamingDiffZone, textAreaRef: (r) => { textAreaRef.current = r @@ -1198,19 +1205,15 @@ class EditCodeService extends Disposable implements IEditCodeService { public startApplying(opts: StartApplyingOpts) { - if (opts.type === 'rewrite') { - const addedDiffZone = this._initializeWriteoverStream(opts) - return addedDiffZone?.diffareaid + const addedDiffArea = this._initializeWriteoverStream(opts) + return addedDiffArea?._URI ?? null } - else if (opts.type === 'searchReplace') { - const addedDiffZone = this._initializeSearchAndReplaceStream(opts) - return addedDiffZone?.diffareaid + const addedDiffArea = this._initializeSearchAndReplaceStream(opts) + return addedDiffArea?._URI ?? null } - - return undefined - + return null } @@ -1708,18 +1711,46 @@ class EditCodeService extends Disposable implements IEditCodeService { this._undoRedoService.undo(uri) } - // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream - interruptStreaming(diffareaid: number) { - const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) return - if (diffArea.type !== 'DiffZone') return - if (!diffArea._streamState.isStreaming) return - this._stopIfStreaming(diffArea) - this._undoHistory(diffArea._URI) + + + isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (!ctrlKZone) return false + if (ctrlKZone.type !== 'CtrlKZone') return false + return !!ctrlKZone._linkedStreamingDiffZone } + isDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') return false + return diffZone._streamState.isStreaming + } + + + // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream + interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') return + if (!diffZone._streamState.isStreaming) return + + this._stopIfStreaming(diffZone) + this._undoHistory(diffZone._URI) + } + + interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) { + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone?.type !== 'CtrlKZone') return + if (!ctrlKZone._linkedStreamingDiffZone) return + + const linkedStreamingDiffZone = this.diffAreaOfId[ctrlKZone._linkedStreamingDiffZone] + if (!linkedStreamingDiffZone) return + if (linkedStreamingDiffZone.type !== 'DiffZone') return + + this.interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + + } diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index 1099e74c..da8f5c55 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -17,7 +17,6 @@ import { IMetricsService } from '../common/metricsService.js'; export type QuickEditPropsType = { diffareaid: number, - initStreamingDiffZoneId: number | null, textAreaRef: (ref: HTMLTextAreaElement | null) => void; onChangeHeight: (height: number) => void; onChangeText: (text: string) => void; 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 d279c631..0c6176ff 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 @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor } from '../util/services.js' +import { useAccessor, useIsURIStreaming } from '../util/services.js' +import { useRefState } from '../util/helpers.js' +import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' enum CopyButtonText { Idle = 'Copy', @@ -50,7 +52,15 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin const metricsService = accessor.get('IMetricsService') - const onApply = useCallback(() => { + const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) + const isStreaming = currStreamingDiffZoneRef.current !== null + const isDisabled = !!isFeatureNameDisabled('Ctrl+K', settingsState) + + useIsDiffZoneStreaming(isDiffAreaStreaming) + + + const onSubmit = useCallback(() => { + const diffareaid = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', @@ -60,7 +70,31 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [metricsService, editCodeService, codeStr]) + + if (isDisabled) return + if (currStreamingDiffZoneRef.current !== null) return + textAreaFnsRef.current?.disable() + + const id = editCodeService.startApplying({ + from: 'QuickEdit', + type: 'rewrite', + diffareaid: diffareaid, + }) + setCurrentlyStreamingDiffZone(id ?? null) + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, editCodeService, diffareaid]) + + const onInterrupt = useCallback(() => { + if (currStreamingDiffZoneRef.current === null) return + editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) + setCurrentlyStreamingDiffZone(null) + textAreaFnsRef.current?.enable() + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, editCodeService]) + + + + + + const isSingleLine = !codeStr.includes('\n') 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 f79c9af9..f00bc251 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 @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; -import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js'; +import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useIsCtrlKZoneStreaming } from '../util/services.js'; import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; @@ -16,7 +16,6 @@ import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/vo export const QuickEditChat = ({ diffareaid, - initStreamingDiffZoneId, onChangeHeight, onChangeText: onChangeText_, textAreaRef: textAreaRef_, @@ -49,28 +48,25 @@ export const QuickEditChat = ({ const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState) - const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) - const isStreaming = currStreamingDiffZoneRef.current !== null + const isStreamingRefState = useIsCtrlKZoneStreaming(diffareaid) const onSubmit = useCallback(() => { if (isDisabled) return - if (currStreamingDiffZoneRef.current !== null) return + if (isStreamingRefState.current) return textAreaFnsRef.current?.disable() - const id = editCodeService.startApplying({ + editCodeService.startApplying({ from: 'QuickEdit', - type:'rewrite', - diffareaid: diffareaid, + type: 'rewrite', + diffareaid, }) - setCurrentlyStreamingDiffZone(id ?? null) - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, editCodeService, diffareaid]) + }, [isStreamingRefState, isDisabled, editCodeService, diffareaid]) const onInterrupt = useCallback(() => { - if (currStreamingDiffZoneRef.current === null) return - editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) - setCurrentlyStreamingDiffZone(null) + if (!isStreamingRefState.current ) return + editCodeService.interruptCtrlKStreaming({ diffareaid }) textAreaFnsRef.current?.enable() - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, editCodeService]) + }, [isStreamingRefState, editCodeService]) const onX = useCallback(() => { @@ -89,7 +85,7 @@ export const QuickEditChat = ({ onSubmit={onSubmit} onAbort={onInterrupt} onClose={onX} - isStreaming={isStreaming} + isStreaming={isStreamingRefState.current} isDisabled={isDisabled} featureName="Ctrl+K" className="py-2 w-full" 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 30351697..fd2d8c9e 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 @@ -557,11 +557,11 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM let setStagingSelections = (s: StagingSelectionItem[]) => { } if (messageIdx !== undefined) { - const [_state, _setState] = chatThreadsService._useCurrentMessageState(messageIdx) + const _state = chatThreadsService.getCurrentMessageState(messageIdx) isBeingEdited = _state.isBeingEdited - setIsBeingEdited = (v) => _setState({ isBeingEdited: v }) stagingSelections = _state.stagingSelections - setStagingSelections = (s) => { _setState({ stagingSelections: s }) } + setIsBeingEdited = (v) => chatThreadsService.setCurrentMessageState(messageIdx, { isBeingEdited: v }) + setStagingSelections = (s) => chatThreadsService.setCurrentMessageState(messageIdx, { stagingSelections: s }) } @@ -780,9 +780,8 @@ export const SidebarChat = () => { const currentThread = chatThreadsService.getCurrentThread() const previousMessages = currentThread?.messages ?? [] - const [_state, _setState] = chatThreadsService._useCurrentThreadState() - const selections = _state.stagingSelections - const setSelections = (s: StagingSelectionItem[]) => { _setState({ stagingSelections: s }) } + const selections = chatThreadsService.getCurrentThread().state.stagingSelections + const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadStagingSelections(s) } // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) @@ -818,7 +817,7 @@ export const SidebarChat = () => { textAreaFnsRef.current?.setValue('') textAreaRef.current?.focus() // focus input after submit - }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, selections, setSelections]) + }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, setSelections]) const onAbort = () => { const threadId = currentThread.id diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 2d516ace..7ca177ce 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -47,6 +47,7 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js' import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js' import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js' +import { URI } from '../../../../../../../base/common/uri.js' @@ -353,3 +354,17 @@ export const useIsDark = () => { return isDark } + + + + +export const useIsCtrlKZoneStreaming = (diffareaid: number) => { + + return { current: true } + +} + + +export const useIsDiffZoneStreaming = (uri: URI) => { + +} diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 39eb8381..722eaa57 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -141,13 +141,11 @@ registerAction2(class extends Action2 { let setSelections = (s: StagingSelectionItem[]) => { } if (focusedMessageIdx === undefined) { - const [state, setState] = chatThreadService._useCurrentThreadState() - selections = state.stagingSelections - setSelections = (s) => setState({ stagingSelections: s }) + selections = chatThreadService.getCurrentThreadStagingSelections() + setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadStagingSelections(s) } else { - const [state, setState] = chatThreadService._useCurrentMessageState(focusedMessageIdx) - selections = state.stagingSelections - setSelections = (s) => setState({ stagingSelections: s }) + selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections + setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s }) } // if matches with existing selection, overwrite (since text may change) From 9005de65a6dd5aa7a79eb3b99391bb65c468e999 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 20:53:09 -0800 Subject: [PATCH 68/92] quick edit stream state? --- .../contrib/void/browser/editCodeService.ts | 103 +++++++++++------- .../src/markdown/ApplyBlockHoverButtons.tsx | 58 ++++------ .../react/src/markdown/ChatMarkdownRender.tsx | 3 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 20 ++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 4 +- .../void/browser/react/src/util/services.tsx | 53 +++++---- 6 files changed, 136 insertions(+), 105 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 657584ed..0d1cc369 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -35,7 +35,7 @@ import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; -import { Emitter } from '../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; @@ -66,7 +66,6 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); - const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { const model = editor.getModel(); @@ -122,6 +121,7 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num } +type AcceptRejectAllState = 'idle' | 'acceptRejectAll' | 'streaming' export type StartApplyingOpts = { @@ -142,7 +142,7 @@ export type AddCtrlKOpts = { } // // TODO diffArea should be removed if we just discovered it has no more diffs in it -// for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { +// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { // const diffArea = this.diffAreaOfId[diffareaid] // if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) { // const { onFinishEdit } = this._addToHistory(uri) @@ -197,10 +197,12 @@ type DiffZone = { isStreaming: true; streamRequestIdRef: { current: string | null }; line: number; + applyBoxId?: string; } | { isStreaming: false; streamRequestIdRef?: undefined; line?: undefined; + applyBoxId?: undefined; }; editorId?: undefined; linkedStreamingDiffZone?: undefined; @@ -253,11 +255,15 @@ export interface IEditCodeService { addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; - isDiffZoneStreaming(opts: { diffareaid: number }): boolean; + // CtrlKZone streaming state isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean; - - interruptDiffZoneStreaming(opts: { diffareaid: number }): void; interruptCtrlKStreaming(opts: { diffareaid: number }): void; + onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>; + + // // DiffZone streaming state + // isApplyBoxIdStreaming(opts: { applyBoxId: string }): boolean; + // interruptApplyBoxId(opts: { applyBoxId: string }): void; + // onDidChangeApplyBoxIdStreaming: Event<{ applyBoxId: string }>; // testDiffs(): void; } @@ -277,10 +283,14 @@ class EditCodeService extends Disposable implements IEditCodeService { // only applies to diffZones // streamingDiffZones: Set = new Set() - private readonly _onDidChangeStreaming = new Emitter<{ uri: URI }>(); - private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); + private readonly _onDidChangeDiffZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + private readonly _onDidAddOrDeleteDiffZone = new Emitter<{ uri: URI }>(); + private readonly _onDidChangeAcceptRejectAllState = new Emitter<{ uri: URI, state: AcceptRejectAllState }>(); // was going to be used, but decided not to + onDidChangeDiffZoneStreaming = this._onDidChangeDiffZoneStreaming.event + onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @@ -315,23 +325,33 @@ class EditCodeService extends Disposable implements IEditCodeService { }) ) - // when a stream starts or ends + // add the accept|reject UI here let removeAcceptRejectAllUI: (() => void) | null = null + this._register(this._onDidChangeAcceptRejectAllState.event(({ uri, state }) => { + if (state === 'acceptRejectAll' && !removeAcceptRejectAllUI) { + removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null + } else { + removeAcceptRejectAllUI?.() + removeAcceptRejectAllUI = null + } + })) + + // when a stream starts or ends const changeUriState = () => { const uri = model.uri const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] .map(diffareaid => this.diffAreaOfId[diffareaid]) .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) - if (diffZones.length !== 0 && !isStreaming && !removeAcceptRejectAllUI) { - removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null - } else { - removeAcceptRejectAllUI?.() - removeAcceptRejectAllUI = null - } + + const state: AcceptRejectAllState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') + this._onDidChangeAcceptRejectAllState.fire({ uri, state }) } - this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) - this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) + this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) + this._register(this._onDidAddOrDeleteDiffZone.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) + + + } // initialize all existing models + initialize when a new model mounts for (let model of this._modelService.getModels()) { initializeModel(model) } @@ -412,7 +432,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _addDiffAreaStylesToURI = (uri: URI) => { const model = this._getModel(uri) - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type === 'DiffZone') { @@ -441,7 +461,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _computeDiffsAndAddStylesToURI = (uri: URI) => { const fullFileText = this._readURI(uri) ?? '' - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type !== 'DiffZone') continue @@ -465,7 +485,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // find all diffzones that aren't streaming const diffZones: DiffZone[] = [] - for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (let diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type !== 'DiffZone') continue if (diffArea._streamState.isStreaming) continue @@ -583,7 +603,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _refreshCtrlKInputs = async (uri: URI) => { - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type !== 'CtrlKZone') continue if (!diffArea._mountInfo) { @@ -852,7 +872,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } this.diffAreasOfURI[uri.fsPath].add(diffareaid) } - this._onDidAddOrDeleteDiffZones.fire({ uri }) + this._onDidAddOrDeleteDiffZone.fire({ uri }) // restore file content const numLines = this._getNumLines(uri) @@ -910,7 +930,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // clears all Diffs (and their styles) and all styles of DiffAreas, etc private _clearAllEffects(uri: URI) { - for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (let diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] this._clearAllDiffAreaEffects(diffArea) } @@ -922,7 +942,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._clearAllDiffAreaEffects(diffZone) delete this.diffAreaOfId[diffZone.diffareaid] this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString()) - this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) + this._onDidAddOrDeleteDiffZone.fire({ uri: diffZone._URI }) } private _deleteTrackingZone(trackingZone: TrackingZone) { @@ -1221,7 +1241,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null { // check if there's overlap with any other diffAreas and return early if there is - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (!diffArea) continue if (!filter?.(diffArea)) continue @@ -1305,8 +1325,8 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZone.fire({ uri }) if (from === 'QuickEdit') { const { diffareaid } = opts @@ -1314,6 +1334,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (ctrlKZone.type !== 'CtrlKZone') return ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid }) } // now handle messages @@ -1347,12 +1368,13 @@ class EditCodeService extends Disposable implements IEditCodeService { const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) if (from === 'QuickEdit') { const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone ctrlKZone._linkedStreamingDiffZone = null + this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid }) this._deleteCtrlKZone(ctrlKZone) } this._refreshStylesAndDiffsInURI(uri) @@ -1476,8 +1498,8 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZone.fire({ uri }) const revertAndContinueHistory = () => { @@ -1516,7 +1538,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._refreshStylesAndDiffsInURI(uri) // delete the tracking zones @@ -1704,7 +1726,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._llmMessageService.abort(streamRequestId) diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) } _undoHistory(uri: URI) { @@ -1715,6 +1737,12 @@ class EditCodeService extends Disposable implements IEditCodeService { + isDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') return false + return diffZone._streamState.isStreaming + } + isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] if (!ctrlKZone) return false @@ -1722,14 +1750,7 @@ class EditCodeService extends Disposable implements IEditCodeService { return !!ctrlKZone._linkedStreamingDiffZone } - isDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return false - return diffZone._streamState.isStreaming - } - - // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { const diffZone = this.diffAreaOfId[diffareaid] if (diffZone?.type !== 'DiffZone') return @@ -1739,6 +1760,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._undoHistory(diffZone._URI) } + // diffareaid of the ctrlKZone (even though the stream state is dictated by the linked diffZone) interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone?.type !== 'CtrlKZone') return @@ -1749,14 +1771,11 @@ class EditCodeService extends Disposable implements IEditCodeService { if (linkedStreamingDiffZone.type !== 'DiffZone') return this.interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) - } - - // public removeDiffZone(diffZone: DiffZone, behavior: 'reject' | 'accept') { // const uri = diffZone._URI // const { onFinishEdit } = this._addToHistory(uri) 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 0c6176ff..6224110b 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 @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor, useIsURIStreaming } from '../util/services.js' +import { useAccessor, useIsDiffZoneStreaming } from '../util/services.js' import { useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' @@ -52,43 +52,33 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin const metricsService = accessor.get('IMetricsService') - const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) - const isStreaming = currStreamingDiffZoneRef.current !== null - const isDisabled = !!isFeatureNameDisabled('Ctrl+K', settingsState) - - useIsDiffZoneStreaming(isDiffAreaStreaming) - - - const onSubmit = useCallback(() => { - - const diffareaid = editCodeService.startApplying({ - from: 'ClickApply', - type: 'searchReplace', - applyStr: codeStr, - }) - - metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + // const isStreaming = useIsDiffZoneStreaming(isDiffAreaStreaming) - if (isDisabled) return - if (currStreamingDiffZoneRef.current !== null) return - textAreaFnsRef.current?.disable() + // const onSubmit = useCallback(() => { - const id = editCodeService.startApplying({ - from: 'QuickEdit', - type: 'rewrite', - diffareaid: diffareaid, - }) - setCurrentlyStreamingDiffZone(id ?? null) - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, editCodeService, diffareaid]) + // const uri = editCodeService.startApplying({ + // from: 'ClickApply', + // type: 'searchReplace', + // applyStr: codeStr, + // }) - const onInterrupt = useCallback(() => { - if (currStreamingDiffZoneRef.current === null) return - editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) - setCurrentlyStreamingDiffZone(null) - textAreaFnsRef.current?.enable() - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, editCodeService]) + // metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + + + + // if (isStreaming) return + + // setCurrentlyStreamingDiffZone(id ?? null) + // }, [isStreaming, editCodeService]) + + // const onInterrupt = useCallback(() => { + // if (currStreamingDiffZoneRef.current === null) return + // editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) + // setCurrentlyStreamingDiffZone(null) + // textAreaFnsRef.current?.enable() + // }, [isStreaming, editCodeService]) @@ -101,7 +91,7 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin return diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index b5378de7..d6af4843 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -9,6 +9,7 @@ import { BlockCode } from './BlockCode.js' import { ChatMessageLocation, } from '../../../aiRegexService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' +import { URI } from '../../../../../../../base/common/uri.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } @@ -33,7 +34,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c } -const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { +const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { // deal with built-in tokens first (assume marked token) 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 f00bc251..44899741 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 @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; -import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useIsCtrlKZoneStreaming } from '../util/services.js'; +import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js'; import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; @@ -48,11 +48,17 @@ export const QuickEditChat = ({ const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState) - const isStreamingRefState = useIsCtrlKZoneStreaming(diffareaid) + + const [isStreamingRef, setIsStreamingRef] = useRefState(false) + useCtrlKZoneStreamingState(useCallback((diffareaid2, isStreaming) => { + if (diffareaid !== diffareaid2) return + setIsStreamingRef(isStreaming) + }, [diffareaid, setIsStreamingRef])) + const onSubmit = useCallback(() => { if (isDisabled) return - if (isStreamingRefState.current) return + if (isStreamingRef.current) return textAreaFnsRef.current?.disable() editCodeService.startApplying({ @@ -60,13 +66,13 @@ export const QuickEditChat = ({ type: 'rewrite', diffareaid, }) - }, [isStreamingRefState, isDisabled, editCodeService, diffareaid]) + }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) const onInterrupt = useCallback(() => { - if (!isStreamingRefState.current ) return + if (!isStreamingRef.current) return editCodeService.interruptCtrlKStreaming({ diffareaid }) textAreaFnsRef.current?.enable() - }, [isStreamingRefState, editCodeService]) + }, [isStreamingRef, editCodeService]) const onX = useCallback(() => { @@ -85,7 +91,7 @@ export const QuickEditChat = ({ onSubmit={onSubmit} onAbort={onInterrupt} onClose={onX} - isStreaming={isStreamingRefState.current} + isStreaming={isStreamingRef.current} isDisabled={isDisabled} featureName="Ctrl+K" className="py-2 w-full" 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 fd2d8c9e..5f7c42ce 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 @@ -552,9 +552,9 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM // global state let isBeingEdited = false - let setIsBeingEdited = (v: boolean) => { } let stagingSelections: StagingSelectionItem[] = [] - let setStagingSelections = (s: StagingSelectionItem[]) => { } + let setIsBeingEdited = (_: boolean) => { } + let setStagingSelections = (_: StagingSelectionItem[]) => { } if (messageIdx !== undefined) { const _state = chatThreadsService.getCurrentMessageState(messageIdx) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 7ca177ce..bf44e045 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -14,10 +14,6 @@ import { VoidUriState } from '../../../voidUriStateService.js'; import { VoidQuickEditState } from '../../../quickEditStateService.js' import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js' - - - - import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js'; import { IModelService } from '../../../../../../../editor/common/services/model.js'; import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js'; @@ -47,7 +43,6 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js' import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js' import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js' -import { URI } from '../../../../../../../base/common/uri.js' @@ -80,6 +75,13 @@ const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: Refresh let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() +const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() + +let diffZoneStreamingState: Record +const diffZoneStreamingStateListeners: Set<(diffareaid: number, state: boolean) => void> = new Set() + + + // must call this before you can use any of the hooks below // this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it! let wasCalled = false @@ -163,7 +165,7 @@ export const _registerServices = (accessor: ServicesAccessor) => { refreshModelService.onDidChangeState((providerName) => { refreshModelState = refreshModelService.state refreshModelStateListeners.forEach(l => l(refreshModelState)) - refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState)) + refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState)) // no state }) ) @@ -175,6 +177,15 @@ export const _registerServices = (accessor: ServicesAccessor) => { }) ) + // no state + disposables.push( + editCodeService.onDidChangeCtrlKZoneStreaming(({ diffareaid }) => { + const isStreaming = editCodeService.isCtrlKZoneStreaming({ diffareaid }) + ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming)) + }) + ) + + return disposables } @@ -341,6 +352,23 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv } +export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boolean) => void) => { + useEffect(() => { + ctrlKZoneStreamingStateListeners.add(listener) + return () => { ctrlKZoneStreamingStateListeners.delete(listener) } + }, [listener]) +} + + +export const useIsDiffZoneStreaming = (diffareaid: number) => { + return { current: true } + +} + + + + + export const useIsDark = () => { const [s, ss] = useState(colorThemeState) useEffect(() => { @@ -355,16 +383,3 @@ export const useIsDark = () => { } - - - -export const useIsCtrlKZoneStreaming = (diffareaid: number) => { - - return { current: true } - -} - - -export const useIsDiffZoneStreaming = (uri: URI) => { - -} From 64ac6d4a1254bc65996d163321c60cd72d9736a7 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 21:09:03 -0800 Subject: [PATCH 69/92] ctrlK --- .../contrib/void/browser/editCodeService.ts | 30 +++++++------------ .../src/quick-edit-tsx/QuickEditChat.tsx | 2 +- .../void/browser/react/src/util/services.tsx | 4 +-- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 0d1cc369..b14c085a 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -287,8 +287,6 @@ class EditCodeService extends Disposable implements IEditCodeService { private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); private readonly _onDidAddOrDeleteDiffZone = new Emitter<{ uri: URI }>(); - private readonly _onDidChangeAcceptRejectAllState = new Emitter<{ uri: URI, state: AcceptRejectAllState }>(); // was going to be used, but decided not to - onDidChangeDiffZoneStreaming = this._onDidChangeDiffZoneStreaming.event onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event @@ -325,19 +323,9 @@ class EditCodeService extends Disposable implements IEditCodeService { }) ) - // add the accept|reject UI here - let removeAcceptRejectAllUI: (() => void) | null = null - this._register(this._onDidChangeAcceptRejectAllState.event(({ uri, state }) => { - if (state === 'acceptRejectAll' && !removeAcceptRejectAllUI) { - removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null - } else { - removeAcceptRejectAllUI?.() - removeAcceptRejectAllUI = null - } - })) - - // when a stream starts or ends - const changeUriState = () => { + // when a stream starts or ends, add/remove the accept|reject UI + let _removeAcceptRejectAllUI: (() => void) | null = null + const updateAcceptRejectAllUI = () => { const uri = model.uri const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] .map(diffareaid => this.diffAreaOfId[diffareaid]) @@ -345,12 +333,16 @@ class EditCodeService extends Disposable implements IEditCodeService { const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) const state: AcceptRejectAllState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') - this._onDidChangeAcceptRejectAllState.fire({ uri, state }) + if (state === 'acceptRejectAll' && !_removeAcceptRejectAllUI) { + _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null + } else { + _removeAcceptRejectAllUI?.() + _removeAcceptRejectAllUI = null + } } - this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) - this._register(this._onDidAddOrDeleteDiffZone.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) - + this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) + this._register(this._onDidAddOrDeleteDiffZone.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) } // initialize all existing models + initialize when a new model mounts 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 44899741..fe70caa3 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 @@ -49,7 +49,7 @@ export const QuickEditChat = ({ const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState) - const [isStreamingRef, setIsStreamingRef] = useRefState(false) + const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCtrlKZoneStreaming({ diffareaid })) useCtrlKZoneStreamingState(useCallback((diffareaid2, isStreaming) => { if (diffareaid !== diffareaid2) return setIsStreamingRef(isStreaming) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index bf44e045..9ea92f0c 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -348,7 +348,7 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv useEffect(() => { refreshModelProviderListeners.add(listener) return () => { refreshModelProviderListeners.delete(listener) } - }, [listener]) + }, [listener, refreshModelProviderListeners]) } @@ -356,7 +356,7 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo useEffect(() => { ctrlKZoneStreamingStateListeners.add(listener) return () => { ctrlKZoneStreamingStateListeners.delete(listener) } - }, [listener]) + }, [listener, ctrlKZoneStreamingStateListeners]) } From 1079893527ae9b8f7fc31cc0ed00ceea3f7c1928 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 23:31:06 -0800 Subject: [PATCH 70/92] add codeBoxId, but now will change to uri --- .../contrib/void/browser/editCodeService.ts | 96 +++++++++++----- .../src/markdown/ApplyBlockHoverButtons.tsx | 106 +++++++++++------- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 + .../void/browser/react/src/util/services.tsx | 21 ++-- 4 files changed, 149 insertions(+), 75 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index b14c085a..9c37821f 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -128,13 +128,16 @@ export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) + chatCodeBoxId: string | null; } | { from: 'ClickApply'; type: 'searchReplace' | 'rewrite'; applyStr: string; + chatCodeBoxId: string | null; } + export type AddCtrlKOpts = { startLine: number, endLine: number, @@ -197,12 +200,11 @@ type DiffZone = { isStreaming: true; streamRequestIdRef: { current: string | null }; line: number; - applyBoxId?: string; + codeBoxId: string | null; } | { isStreaming: false; streamRequestIdRef?: undefined; line?: undefined; - applyBoxId?: undefined; }; editorId?: undefined; linkedStreamingDiffZone?: undefined; @@ -260,10 +262,10 @@ export interface IEditCodeService { interruptCtrlKStreaming(opts: { diffareaid: number }): void; onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>; - // // DiffZone streaming state - // isApplyBoxIdStreaming(opts: { applyBoxId: string }): boolean; - // interruptApplyBoxId(opts: { applyBoxId: string }): void; - // onDidChangeApplyBoxIdStreaming: Event<{ applyBoxId: string }>; + // // DiffZone codeBoxId streaming state + isCodeBoxIdStreaming(opts: { codeBoxId: string }): boolean; + interruptCodeBoxId(opts: { codeBoxId: string }): void; + onDidChangeCodeBoxIdStreaming: Event<{ codeBoxId: string }>; // testDiffs(): void; } @@ -284,12 +286,14 @@ class EditCodeService extends Disposable implements IEditCodeService { // only applies to diffZones // streamingDiffZones: Set = new Set() private readonly _onDidChangeDiffZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); - private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); - private readonly _onDidAddOrDeleteDiffZone = new Emitter<{ uri: URI }>(); + private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); - onDidChangeDiffZoneStreaming = this._onDidChangeDiffZoneStreaming.event + private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event + private readonly _onDidChangeCodeBoxIdStreaming = new Emitter<{ uri: URI; diffareaid: number; codeBoxId: string }>(); + onDidChangeCodeBoxIdStreaming = this._onDidChangeCodeBoxIdStreaming.event + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @ICodeEditorService private readonly _editorService: ICodeEditorService, @@ -342,7 +346,17 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) - this._register(this._onDidAddOrDeleteDiffZone.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) + this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) + + // codeBoxId + this._register(this._onDidChangeDiffZoneStreaming.event(({ diffareaid }) => { + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') return + if (!diffZone._streamState.isStreaming) return + const { codeBoxId } = diffZone._streamState + if (codeBoxId === null) return + this._onDidChangeCodeBoxIdStreaming.fire({ uri: model.uri, codeBoxId, diffareaid }) + })) } // initialize all existing models + initialize when a new model mounts @@ -864,7 +878,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } this.diffAreasOfURI[uri.fsPath].add(diffareaid) } - this._onDidAddOrDeleteDiffZone.fire({ uri }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) // restore file content const numLines = this._getNumLines(uri) @@ -934,7 +948,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._clearAllDiffAreaEffects(diffZone) delete this.diffAreaOfId[diffZone.diffareaid] this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString()) - this._onDidAddOrDeleteDiffZone.fire({ uri: diffZone._URI }) + this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) } private _deleteTrackingZone(trackingZone: TrackingZone) { @@ -1252,7 +1266,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - const { from } = opts + const { from, chatCodeBoxId } = opts let startLine: number let endLine: number @@ -1312,13 +1326,14 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, + codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZone.fire({ uri }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) if (from === 'QuickEdit') { const { diffareaid } = opts @@ -1436,7 +1451,8 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { + private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { + const { applyStr, chatCodeBoxId } = opts const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1485,13 +1501,14 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, + codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZone.fire({ uri }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) const revertAndContinueHistory = () => { @@ -1729,12 +1746,16 @@ class EditCodeService extends Disposable implements IEditCodeService { - isDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + _interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return false - return diffZone._streamState.isStreaming + if (diffZone?.type !== 'DiffZone') return + if (!diffZone._streamState.isStreaming) return + + this._stopIfStreaming(diffZone) + this._undoHistory(diffZone._URI) } + isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] if (!ctrlKZone) return false @@ -1743,15 +1764,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return - if (!diffZone._streamState.isStreaming) return - - this._stopIfStreaming(diffZone) - this._undoHistory(diffZone._URI) - } - // diffareaid of the ctrlKZone (even though the stream state is dictated by the linked diffZone) interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] @@ -1762,11 +1774,37 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!linkedStreamingDiffZone) return if (linkedStreamingDiffZone.type !== 'DiffZone') return - this.interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + this._interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) } + isCodeBoxIdStreaming({ codeBoxId }: { codeBoxId: string }) { + // brute force is OK for now + for (const diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + if (!diffArea) continue + if (diffArea.type !== 'DiffZone') continue + if (!diffArea._streamState.isStreaming) continue + if (diffArea._streamState.codeBoxId === codeBoxId) return true + } + return false + } + + interruptCodeBoxId({ codeBoxId }: { codeBoxId: string }) { + // brute force for now is OK + for (const diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + if (!diffArea) continue + if (diffArea.type !== 'DiffZone') continue + if (!diffArea._streamState.isStreaming) continue + if (diffArea._streamState.codeBoxId === codeBoxId) { + this._interruptDiffZoneStreaming({ diffareaid: diffArea.diffareaid }) + return + } + } + } + // public removeDiffZone(diffZone: DiffZone, behavior: 'reject' | 'accept') { // const uri = diffZone._URI 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 6224110b..ecd5d7d9 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 @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor, useIsDiffZoneStreaming } from '../util/services.js' +import { useAccessor, useCodeBoxIdStreamingState, useSettingsState } from '../util/services.js' import { useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' @@ -44,54 +44,35 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { } +const useStreamStateRef = ({ codeBoxId }: { codeBoxId: string | null }) => { + const accessor = useAccessor() + const editCodeService = accessor.get('IEditCodeService') + const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCodeBoxIdStreaming({ codeBoxId })) + useCodeBoxIdStreamingState(useCallback((codeBoxId2, isStreaming) => { + if (codeBoxId !== codeBoxId2) return + setIsStreamingRef(isStreaming) + }, [codeBoxId, setIsStreamingRef])) + return [isStreamingRef, setIsStreamingRef] as const +} -const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string }) => { + + +const StopButton = ({ codeBoxId }: { codeBoxId: string }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') + const settingsState = useSettingsState() - // const isStreaming = useIsDiffZoneStreaming(isDiffAreaStreaming) + const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) - // const onSubmit = useCallback(() => { - - // const uri = editCodeService.startApplying({ - // from: 'ClickApply', - // type: 'searchReplace', - // applyStr: codeStr, - // }) - - // metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - - - - // if (isStreaming) return - - // setCurrentlyStreamingDiffZone(id ?? null) - // }, [isStreaming, editCodeService]) - - // const onInterrupt = useCallback(() => { - // if (currStreamingDiffZoneRef.current === null) return - // editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) - // setCurrentlyStreamingDiffZone(null) - // textAreaFnsRef.current?.enable() - // }, [isStreaming, editCodeService]) - - - - - - - - const isSingleLine = !codeStr.includes('\n') - return @@ -103,8 +84,57 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string | null }) => { + + + + const accessor = useAccessor() + + const editCodeService = accessor.get('IEditCodeService') + const metricsService = accessor.get('IMetricsService') + + const settingsState = useSettingsState() + + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) + + const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) + + const onSubmit = useCallback(() => { + if (isDisabled) return + if (isStreamingRef.current) return + editCodeService.startApplying({ + from: 'ClickApply', + type: 'searchReplace', + applyStr: codeStr, + chatCodeBoxId: codeBoxId, + }) + metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + }, [isStreamingRef, editCodeService, codeBoxId, codeStr, metricsService]) + + + const onInterrupt = useCallback(() => { + if (isStreamingRef.current) return + if (codeBoxId === null) return + editCodeService.interruptCodeBoxId({ codeBoxId, }) + metricsService.capture('Stop Apply', {}) + }, [isStreamingRef, editCodeService, codeBoxId, metricsService]) + + + + const isSingleLine = !codeStr.includes('\n') + + const applyButton = + + + return <> - - {codeBoxId !== null && } + {!isStreamingRef.current && } + {!isStreamingRef.current && codeBoxId !== null && } + {!isStreamingRef.current && } } 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 fe70caa3..9e8f674f 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 @@ -65,6 +65,7 @@ export const QuickEditChat = ({ from: 'QuickEdit', type: 'rewrite', diffareaid, + chatCodeBoxId: null, }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 9ea92f0c..6e1f5102 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -76,9 +76,7 @@ let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() - -let diffZoneStreamingState: Record -const diffZoneStreamingStateListeners: Set<(diffareaid: number, state: boolean) => void> = new Set() +const codeBoxIdStreamingStateListeners: Set<(codeBoxId: string, s: boolean) => void> = new Set() @@ -184,6 +182,12 @@ export const _registerServices = (accessor: ServicesAccessor) => { ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming)) }) ) + disposables.push( + editCodeService.onDidChangeCodeBoxIdStreaming(({ codeBoxId }) => { + const isStreaming = editCodeService.isCodeBoxIdStreaming({ codeBoxId }) + codeBoxIdStreamingStateListeners.forEach(l => l(codeBoxId, isStreaming)) + }) + ) @@ -351,7 +355,6 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv }, [listener, refreshModelProviderListeners]) } - export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boolean) => void) => { useEffect(() => { ctrlKZoneStreamingStateListeners.add(listener) @@ -359,16 +362,18 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo }, [listener, ctrlKZoneStreamingStateListeners]) } - -export const useIsDiffZoneStreaming = (diffareaid: number) => { - return { current: true } - +export const useCodeBoxIdStreamingState = (listener: (codeBoxId: string, s: boolean) => void) => { + useEffect(() => { + codeBoxIdStreamingStateListeners.add(listener) + return () => { codeBoxIdStreamingStateListeners.delete(listener) } + }, [listener, codeBoxIdStreamingStateListeners]) } + export const useIsDark = () => { const [s, ss] = useState(colorThemeState) useEffect(() => { From 63b71dec24d18e6f3169b05b268bebb9463d3fd3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 00:40:46 -0800 Subject: [PATCH 71/92] should work, just need to debug --- .../contrib/void/browser/editCodeService.ts | 110 ++++++++------- .../src/markdown/ApplyBlockHoverButtons.tsx | 127 +++++++++++------- .../react/src/markdown/ChatMarkdownRender.tsx | 6 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 2 +- .../void/browser/react/src/util/services.tsx | 25 ++-- 5 files changed, 144 insertions(+), 126 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9c37821f..bdecb940 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -121,19 +121,19 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num } -type AcceptRejectAllState = 'idle' | 'acceptRejectAll' | 'streaming' +export type URIStreamState = 'idle' | 'acceptRejectAll' | 'streaming' export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) - chatCodeBoxId: string | null; + chatApplyBoxId: string | null; } | { from: 'ClickApply'; type: 'searchReplace' | 'rewrite'; applyStr: string; - chatCodeBoxId: string | null; + chatApplyBoxId: string | null; } @@ -177,6 +177,7 @@ type CommonZoneProps = { type CtrlKZone = { type: 'CtrlKZone'; originalCode?: undefined; + chatApplyBoxId?: undefined; editorId: string; // the editor the input lives on @@ -196,11 +197,11 @@ type DiffZone = { type: 'DiffZone', originalCode: string; _diffOfId: Record; // diffid -> diff in this DiffArea + chatApplyBoxId: string | null; _streamState: { isStreaming: true; streamRequestIdRef: { current: string | null }; line: number; - codeBoxId: string | null; } | { isStreaming: false; streamRequestIdRef?: undefined; @@ -219,6 +220,7 @@ type TrackingZone = { originalCode?: undefined; editorId?: undefined; _removeStylesFns?: undefined; + chatApplyBoxId?: undefined; } & CommonZoneProps @@ -232,6 +234,7 @@ const diffAreaSnapshotKeys = [ 'startLine', 'endLine', 'editorId', + 'chatApplyBoxId', ] as const satisfies (keyof DiffArea)[] @@ -256,6 +259,7 @@ export interface IEditCodeService { addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; + removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void; // CtrlKZone streaming state isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean; @@ -263,9 +267,9 @@ export interface IEditCodeService { onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>; // // DiffZone codeBoxId streaming state - isCodeBoxIdStreaming(opts: { codeBoxId: string }): boolean; - interruptCodeBoxId(opts: { codeBoxId: string }): void; - onDidChangeCodeBoxIdStreaming: Event<{ codeBoxId: string }>; + getURIStreamState(opts: { uri: URI | null }): URIStreamState; + interruptURIStreaming(opts: { uri: URI }): void; + onDidChangeURIStreamState: Event<{ uri: URI; state: URIStreamState }>; // testDiffs(): void; } @@ -291,8 +295,11 @@ class EditCodeService extends Disposable implements IEditCodeService { private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event - private readonly _onDidChangeCodeBoxIdStreaming = new Emitter<{ uri: URI; diffareaid: number; codeBoxId: string }>(); - onDidChangeCodeBoxIdStreaming = this._onDidChangeCodeBoxIdStreaming.event + private readonly _onDidChangeURIStreamState = new Emitter<{ uri: URI; state: URIStreamState }>(); + onDidChangeURIStreamState = this._onDidChangeURIStreamState.event + + + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @@ -327,36 +334,30 @@ class EditCodeService extends Disposable implements IEditCodeService { }) ) - // when a stream starts or ends, add/remove the accept|reject UI - let _removeAcceptRejectAllUI: (() => void) | null = null + // when a stream starts or ends, fire the event for onDidChangeURIStreamState + let prevStreamState = this.getURIStreamState({ uri: model.uri }) const updateAcceptRejectAllUI = () => { - const uri = model.uri - const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] - .map(diffareaid => this.diffAreaOfId[diffareaid]) - .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') - const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) + const state = this.getURIStreamState({ uri: model.uri }) + if (prevStreamState === state) return + this._onDidChangeURIStreamState.fire({ uri: model.uri, state }) + } - const state: AcceptRejectAllState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') + // add/remove the accept|reject UI + let _removeAcceptRejectAllUI: (() => void) | null = null + this._register(this._onDidChangeURIStreamState.event(({ uri: uri_ }) => { + if (uri_.fsPath !== model.uri.fsPath) return + const state = this.getURIStreamState({ uri: model.uri }) if (state === 'acceptRejectAll' && !_removeAcceptRejectAllUI) { - _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null + _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null } else { _removeAcceptRejectAllUI?.() _removeAcceptRejectAllUI = null } - } + })) this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) - // codeBoxId - this._register(this._onDidChangeDiffZoneStreaming.event(({ diffareaid }) => { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return - if (!diffZone._streamState.isStreaming) return - const { codeBoxId } = diffZone._streamState - if (codeBoxId === null) return - this._onDidChangeCodeBoxIdStreaming.fire({ uri: model.uri, codeBoxId, diffareaid }) - })) } // initialize all existing models + initialize when a new model mounts @@ -505,7 +506,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const buttonsWidget = new AcceptAllRejectAllWidget({ editor, onAcceptAll: () => { - this.removeDiffAreas({ uri, behavior: 'keep', removeCtrlKs: false }) + this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) this._metricsService.capture('Accept All', {}) }, onRejectAll: () => { @@ -1266,7 +1267,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - const { from, chatCodeBoxId } = opts + const { from, chatApplyBoxId } = opts let startLine: number let endLine: number @@ -1318,6 +1319,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', + chatApplyBoxId, originalCode, startLine, endLine, @@ -1326,7 +1328,6 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, - codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), @@ -1452,7 +1453,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { - const { applyStr, chatCodeBoxId } = opts + const { applyStr, chatApplyBoxId } = opts const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1493,6 +1494,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', + chatApplyBoxId, originalCode: originalFileCode, startLine, endLine, @@ -1501,7 +1503,6 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, - codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), @@ -1555,7 +1556,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._deleteTrackingZone(trackingZone) onFinishEdit() - shouldSendAnotherMessage = false } // refresh now in case onText takes a while to get 1st message @@ -1746,7 +1746,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - _interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + _interruptSingleDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { const diffZone = this.diffAreaOfId[diffareaid] if (diffZone?.type !== 'DiffZone') return if (!diffZone._streamState.isStreaming) return @@ -1774,35 +1774,33 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!linkedStreamingDiffZone) return if (linkedStreamingDiffZone.type !== 'DiffZone') return - this._interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + this._interruptSingleDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) } - isCodeBoxIdStreaming({ codeBoxId }: { codeBoxId: string }) { - // brute force is OK for now - for (const diffareaid in this.diffAreaOfId) { - const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) continue - if (diffArea.type !== 'DiffZone') continue - if (!diffArea._streamState.isStreaming) continue - if (diffArea._streamState.codeBoxId === codeBoxId) return true - } - return false + + getURIStreamState = ({ uri }: { uri: URI | null }) => { + if (uri === null) return 'idle' + + const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] + .map(diffareaid => this.diffAreaOfId[diffareaid]) + .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') + const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) + + const state: URIStreamState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') + return state } - interruptCodeBoxId({ codeBoxId }: { codeBoxId: string }) { + interruptURIStreaming({ uri }: { uri: URI }) { // brute force for now is OK - for (const diffareaid in this.diffAreaOfId) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) continue - if (diffArea.type !== 'DiffZone') continue + if (diffArea?.type !== 'DiffZone') continue if (!diffArea._streamState.isStreaming) continue - if (diffArea._streamState.codeBoxId === codeBoxId) { - this._interruptDiffZoneStreaming({ diffareaid: diffArea.diffareaid }) - return - } + this._stopIfStreaming(diffArea) } + this._undoHistory(uri) } @@ -1829,7 +1827,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // remove a batch of diffareas all at once (and handle accept/reject of their diffs) - public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'keep' }) { + public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { const diffareaids = this.diffAreasOfURI[uri.fsPath] if (diffareaids.size === 0) return // do nothing @@ -1842,7 +1840,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type == 'DiffZone') { if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea) - else if (behavior === 'keep') this._deleteDiffZone(diffArea) + else if (behavior === 'accept') this._deleteDiffZone(diffArea) } else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) { this._deleteCtrlKZone(diffArea) 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 ecd5d7d9..50f14775 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 @@ -1,7 +1,8 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor, useCodeBoxIdStreamingState, useSettingsState } from '../util/services.js' +import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js' import { useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' +import { URI } from '../../../../../../../base/common/uri.js' enum CopyButtonText { Idle = 'Copy', @@ -44,80 +45,71 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { } -const useStreamStateRef = ({ codeBoxId }: { codeBoxId: string | null }) => { - const accessor = useAccessor() - const editCodeService = accessor.get('IEditCodeService') - const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCodeBoxIdStreaming({ codeBoxId })) - useCodeBoxIdStreamingState(useCallback((codeBoxId2, isStreaming) => { - if (codeBoxId !== codeBoxId2) return - setIsStreamingRef(isStreaming) - }, [codeBoxId, setIsStreamingRef])) - return [isStreamingRef, setIsStreamingRef] as const + + + +// state persisted for duration of react only +const streamingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } +const useStreamingURIOfApplyBoxId = (applyBoxId: string | null) => { + const [_, ss] = useState(0) + const uri = applyBoxId === null ? null : streamingURIOfApplyBoxIdRef.current[applyBoxId] + const setUri = useCallback((uri: URI | null) => { + if (applyBoxId === null) return + ss(c => c + 1) + if (uri === null) { + delete streamingURIOfApplyBoxIdRef.current[applyBoxId] + } + else { + streamingURIOfApplyBoxIdRef.current = { + ...streamingURIOfApplyBoxIdRef.current, + [applyBoxId]: uri, + } + } + }, [applyBoxId]) + return [uri, setUri] as const } +export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string | null }) => { -const StopButton = ({ codeBoxId }: { codeBoxId: string }) => { - const accessor = useAccessor() - - const editCodeService = accessor.get('IEditCodeService') - const metricsService = accessor.get('IMetricsService') const settingsState = useSettingsState() - const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) - - - - return - -} - - - - - -export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string | null }) => { - - + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || applyBoxId === null const accessor = useAccessor() - const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - const settingsState = useSettingsState() + // get streaming URI of this applyBlockId (cached in react) + const [appliedURI, setAppliedURI] = useStreamingURIOfApplyBoxId(applyBoxId) - const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) + // get stream state of this URI + const [streamStateRef, setStreamState] = useRefState(editCodeService.getURIStreamState({ uri: appliedURI ?? null })) + useURIStreamState(useCallback((uri, streamState) => { + if (appliedURI?.fsPath !== uri.fsPath) return + setStreamState(streamState) + }, [appliedURI, setStreamState])) - const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) const onSubmit = useCallback(() => { if (isDisabled) return - if (isStreamingRef.current) return - editCodeService.startApplying({ + const uri = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, - chatCodeBoxId: codeBoxId, + chatApplyBoxId: applyBoxId, }) + setAppliedURI(uri) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [isStreamingRef, editCodeService, codeBoxId, codeStr, metricsService]) + }, [streamStateRef, setAppliedURI, editCodeService, applyBoxId, codeStr, metricsService]) const onInterrupt = useCallback(() => { - if (isStreamingRef.current) return - if (codeBoxId === null) return - editCodeService.interruptCodeBoxId({ codeBoxId, }) + if (!appliedURI) return + editCodeService.interruptURIStreaming({ uri: appliedURI, }) metricsService.capture('Stop Apply', {}) - }, [isStreamingRef, editCodeService, codeBoxId, metricsService]) - + }, [streamStateRef, editCodeService, appliedURI, metricsService]) const isSingleLine = !codeStr.includes('\n') @@ -130,11 +122,42 @@ export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string Apply + const stopButton = + + const acceptRejectButtons = <> + + + return <> - {!isStreamingRef.current && } - {!isStreamingRef.current && codeBoxId !== null && } - {!isStreamingRef.current && } + {streamStateRef.current !== 'streaming' && } + {streamStateRef.current === 'idle' && !isDisabled && applyButton} + {streamStateRef.current === 'streaming' && stopButton} + {streamStateRef.current === 'acceptRejectAll' && acceptRejectButtons} } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index d6af4843..f6d08287 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -14,7 +14,7 @@ import { URI } from '../../../../../../../base/common/uri.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } -const getCodeBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { +const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { return `${threadId}-${messageIdx}-${tokenIdx}` } @@ -46,7 +46,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: if (t.type === "code") { - const codeBoxId = chatMessageLocation ? getCodeBoxId({ + const applyBoxId = chatMessageLocation ? getApplyBoxId({ threadId: chatMessageLocation.threadId, messageIdx: chatMessageLocation.messageIdx, tokenIdx: tokenIdx, @@ -55,7 +55,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: return } + buttonsOnHover={} /> } 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 9e8f674f..1fbcc303 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 @@ -65,7 +65,7 @@ export const QuickEditChat = ({ from: 'QuickEdit', type: 'rewrite', diffareaid, - chatCodeBoxId: null, + chatApplyBoxId: null, }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 6e1f5102..5e164428 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js' import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js' import { IDisposable } from '../../../../../../../base/common/lifecycle.js' @@ -24,7 +24,7 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js'; import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'; -import { IEditCodeService } from '../../../editCodeService.js'; +import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'; import { IVoidUriStateService } from '../../../voidUriStateService.js'; import { IQuickEditStateService } from '../../../quickEditStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; @@ -43,6 +43,7 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js' import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js' import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js' +import { URI } from '../../../../../../../base/common/uri.js' @@ -76,7 +77,7 @@ let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() -const codeBoxIdStreamingStateListeners: Set<(codeBoxId: string, s: boolean) => void> = new Set() +const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set() @@ -183,9 +184,9 @@ export const _registerServices = (accessor: ServicesAccessor) => { }) ) disposables.push( - editCodeService.onDidChangeCodeBoxIdStreaming(({ codeBoxId }) => { - const isStreaming = editCodeService.isCodeBoxIdStreaming({ codeBoxId }) - codeBoxIdStreamingStateListeners.forEach(l => l(codeBoxId, isStreaming)) + editCodeService.onDidChangeURIStreamState(({ uri }) => { + const isStreaming = editCodeService.getURIStreamState({ uri }) + uriStreamingStateListeners.forEach(l => l(uri, isStreaming)) }) ) @@ -362,18 +363,14 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo }, [listener, ctrlKZoneStreamingStateListeners]) } -export const useCodeBoxIdStreamingState = (listener: (codeBoxId: string, s: boolean) => void) => { +export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => { useEffect(() => { - codeBoxIdStreamingStateListeners.add(listener) - return () => { codeBoxIdStreamingStateListeners.delete(listener) } - }, [listener, codeBoxIdStreamingStateListeners]) + uriStreamingStateListeners.add(listener) + return () => { uriStreamingStateListeners.delete(listener) } + }, [listener, uriStreamingStateListeners]) } - - - - export const useIsDark = () => { const [s, ss] = useState(colorThemeState) useEffect(() => { From 19ebf4a1a2421f075734f9dac15e99e8168ab41c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 03:03:05 -0800 Subject: [PATCH 72/92] seems to work! some annoying react state stuff.. --- .../contrib/void/browser/chatThreadService.ts | 3 +- .../contrib/void/browser/editCodeService.ts | 25 ++++--- .../browser/helpers/extractCodeFromResult.ts | 10 ++- .../src/markdown/ApplyBlockHoverButtons.tsx | 74 +++++++++---------- .../react/src/markdown/ChatMarkdownRender.tsx | 2 +- 5 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index c325fc8e..e6a52c54 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -17,6 +17,7 @@ import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, import { toLLMChatMessage } from '../common/llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -123,7 +124,7 @@ export type ThreadStreamState = { const newThreadObject = () => { const now = new Date().toISOString() return { - id: new Date().getTime().toString(), + id: generateUuid(), createdAt: now, lastModified: now, messages: [], diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index bdecb940..a76ada5e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -338,23 +338,24 @@ class EditCodeService extends Disposable implements IEditCodeService { let prevStreamState = this.getURIStreamState({ uri: model.uri }) const updateAcceptRejectAllUI = () => { const state = this.getURIStreamState({ uri: model.uri }) - if (prevStreamState === state) return + let prevStateActual = prevStreamState + prevStreamState = state + if (state === prevStateActual) return this._onDidChangeURIStreamState.fire({ uri: model.uri, state }) } - // add/remove the accept|reject UI + let _removeAcceptRejectAllUI: (() => void) | null = null - this._register(this._onDidChangeURIStreamState.event(({ uri: uri_ }) => { - if (uri_.fsPath !== model.uri.fsPath) return - const state = this.getURIStreamState({ uri: model.uri }) - if (state === 'acceptRejectAll' && !_removeAcceptRejectAllUI) { - _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null + this._register(this._onDidChangeURIStreamState.event(({ uri, state }) => { + if (uri.fsPath !== model.uri.fsPath) return + if (state === 'acceptRejectAll') { + if (!_removeAcceptRejectAllUI) + _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null } else { _removeAcceptRejectAllUI?.() _removeAcceptRejectAllUI = null } })) - this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) @@ -1666,8 +1667,10 @@ class EditCodeService extends Disposable implements IEditCodeService { this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) oldBlocks = blocks // oldblocks is only used if writingFinal - const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] - diffZone._streamState.line = currentEndLine + // const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable + // diffZone._streamState.line = currentEndLine + diffZone._streamState.line = latestStreamLocationMutable.line + } // end for @@ -1682,7 +1685,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const blocks = extractSearchReplaceBlocks(fullText) if (blocks.length === 0) { - this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) + this._notificationService.info(`Void: We ran Apply, but the LLM didn't output any changes.`) } // writeover the whole file diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index b7665eca..cd3276ff 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -201,7 +201,7 @@ export const extractSearchReplaceBlocks = (str: string) => { const ORIGINAL_ = ORIGINAL + `\n` const DIVIDER_ = '\n' + DIVIDER + `\n` - const FINAL_ = '\n' + FINAL + // logic for FINAL_ is slightly more complicated - should be '\n' + FINAL, but that ignores if the final output is empty const blocks: ExtractedSearchReplaceBlock[] = [] @@ -229,7 +229,13 @@ export const extractSearchReplaceBlocks = (str: string) => { i = dividerStart // wrote ===== - let finalStart = str.indexOf(FINAL_, i) + + + const finalStartA = str.indexOf(FINAL, i) + const finalStartB = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive + const FINAL_ = finalStartB !== -1 ? '\n' + FINAL : FINAL + let finalStart = finalStartB !== -1 ? finalStartB : finalStartA + if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_) blocks.push({ 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 50f14775..94c11af0 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 @@ -32,7 +32,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { .then(() => { setCopyButtonText(CopyButtonText.Copied) }) .catch(() => { setCopyButtonText(CopyButtonText.Error) }) metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only - }, [metricsService, clipboardService, codeStr]) + }, [metricsService, clipboardService, codeStr, setCopyButtonText]) const isSingleLine = !codeStr.includes('\n') @@ -49,67 +49,60 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { // state persisted for duration of react only -const streamingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } -const useStreamingURIOfApplyBoxId = (applyBoxId: string | null) => { - const [_, ss] = useState(0) - const uri = applyBoxId === null ? null : streamingURIOfApplyBoxIdRef.current[applyBoxId] - const setUri = useCallback((uri: URI | null) => { - if (applyBoxId === null) return - ss(c => c + 1) - if (uri === null) { - delete streamingURIOfApplyBoxIdRef.current[applyBoxId] - } - else { - streamingURIOfApplyBoxIdRef.current = { - ...streamingURIOfApplyBoxIdRef.current, - [applyBoxId]: uri, - } - } - }, [applyBoxId]) - return [uri, setUri] as const -} +const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } -export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string | null }) => { +export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => { + console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef) const settingsState = useSettingsState() - - const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || applyBoxId === null + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - // get streaming URI of this applyBlockId (cached in react) - const [appliedURI, setAppliedURI] = useStreamingURIOfApplyBoxId(applyBoxId) + const [applyingUriRef, setApplyingUri_] = useRefState(applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null) + const [streamStateRef, setStreamState_] = useRefState(editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null })) - // get stream state of this URI - const [streamStateRef, setStreamState] = useRefState(editCodeService.getURIStreamState({ uri: appliedURI ?? null })) - useURIStreamState(useCallback((uri, streamState) => { - if (appliedURI?.fsPath !== uri.fsPath) return - setStreamState(streamState) - }, [appliedURI, setStreamState])) + const setApplyingUri = useCallback((uri: URI | null) => { // switched the box's URI to whatever they clicked on most recently + setApplyingUri_(uri) + const newStreamState = editCodeService.getURIStreamState({ uri }) + if (uri) applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri + setStreamState_(newStreamState) + }, [applyBoxId, setApplyingUri_, editCodeService, setStreamState_]) + // listen for stream updates + useURIStreamState( + useCallback((uri, streamState) => { + const shouldUpdate = applyingUriRef.current?.fsPath === uri.fsPath + if (!shouldUpdate) return + setStreamState_(streamState) // editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null }) + }, [applyingUriRef, setStreamState_]) + ) const onSubmit = useCallback(() => { if (isDisabled) return + if (streamStateRef.current === 'streaming') return const uri = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, chatApplyBoxId: applyBoxId, }) - setAppliedURI(uri) + setApplyingUri(uri) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [streamStateRef, setAppliedURI, editCodeService, applyBoxId, codeStr, metricsService]) + }, [editCodeService, applyBoxId, codeStr, metricsService, isDisabled, streamStateRef, setApplyingUri]) const onInterrupt = useCallback(() => { - if (!appliedURI) return - editCodeService.interruptURIStreaming({ uri: appliedURI, }) + if (streamStateRef.current !== 'streaming') return + if (!applyingUriRef.current) return + + editCodeService.interruptURIStreaming({ uri: applyingUriRef.current, }) metricsService.capture('Stop Apply', {}) - }, [streamStateRef, editCodeService, appliedURI, metricsService]) + }, [editCodeService, metricsService, streamStateRef, applyingUriRef]) const isSingleLine = !codeStr.includes('\n') @@ -135,8 +128,8 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`} onClick={() => { - if (!appliedURI) return - editCodeService.removeDiffAreas({ uri: appliedURI, behavior: 'accept', removeCtrlKs: false }) + if (!applyingUriRef.current) return + editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'accept', removeCtrlKs: false }) }} > Accept @@ -145,14 +138,15 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`} onClick={() => { - if (!appliedURI) return - editCodeService.removeDiffAreas({ uri: appliedURI, behavior: 'reject', removeCtrlKs: false }) + if (!applyingUriRef.current) return + editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'reject', removeCtrlKs: false }) }} > Reject + console.log('streamStateRef.current', streamStateRef.current) return <> {streamStateRef.current !== 'streaming' && } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index f6d08287..320dccbb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -55,7 +55,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: return } + buttonsOnHover={applyBoxId && } /> } From 16f6181395c3b7814e8352973ec04dd346b19470 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 03:23:44 -0800 Subject: [PATCH 73/92] + --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5f7c42ce..3c95f434 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 @@ -590,7 +590,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM _mustInitialize.current = false } - }, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) + }, [chatMessage, role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) const EditSymbol = mode === 'display' ? Pencil : X const onOpenEdit = () => { setIsBeingEdited(true) @@ -631,7 +631,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM // stream the edit const userMessage = textAreaRefState.value; - await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx }) + await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, }) } const onAbort = () => { From ea10765abd684d7ebd100fe1e5ddd5253b1dcb0a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 03:41:16 -0800 Subject: [PATCH 74/92] remove applybox stuff from editCode --- .../contrib/void/browser/editCodeService.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index a76ada5e..0f92d4ef 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -128,12 +128,10 @@ export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) - chatApplyBoxId: string | null; } | { from: 'ClickApply'; type: 'searchReplace' | 'rewrite'; applyStr: string; - chatApplyBoxId: string | null; } @@ -177,7 +175,6 @@ type CommonZoneProps = { type CtrlKZone = { type: 'CtrlKZone'; originalCode?: undefined; - chatApplyBoxId?: undefined; editorId: string; // the editor the input lives on @@ -197,7 +194,6 @@ type DiffZone = { type: 'DiffZone', originalCode: string; _diffOfId: Record; // diffid -> diff in this DiffArea - chatApplyBoxId: string | null; _streamState: { isStreaming: true; streamRequestIdRef: { current: string | null }; @@ -220,7 +216,6 @@ type TrackingZone = { originalCode?: undefined; editorId?: undefined; _removeStylesFns?: undefined; - chatApplyBoxId?: undefined; } & CommonZoneProps @@ -234,7 +229,6 @@ const diffAreaSnapshotKeys = [ 'startLine', 'endLine', 'editorId', - 'chatApplyBoxId', ] as const satisfies (keyof DiffArea)[] @@ -1268,7 +1262,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - const { from, chatApplyBoxId } = opts + const { from } = opts let startLine: number let endLine: number @@ -1320,7 +1314,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', - chatApplyBoxId, originalCode, startLine, endLine, @@ -1454,7 +1447,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { - const { applyStr, chatApplyBoxId } = opts + const { applyStr } = opts const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1495,7 +1488,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', - chatApplyBoxId, originalCode: originalFileCode, startLine, endLine, From 2ecce822f384fed964cb2f0464e06cf240a8cd73 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 04:30:17 -0800 Subject: [PATCH 75/92] fix react - tedious --- .../src/markdown/ApplyBlockHoverButtons.tsx | 62 +++++++++---------- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 - 2 files changed, 31 insertions(+), 32 deletions(-) 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 94c11af0..8c9f26d1 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 @@ -3,6 +3,7 @@ import { useAccessor, useURIStreamState, useSettingsState } from '../util/servic import { useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { URI } from '../../../../../../../base/common/uri.js' +import { IEditCodeService, URIStreamState } from '../../../editCodeService.js' enum CopyButtonText { Idle = 'Copy', @@ -52,6 +53,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } + export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => { console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef) @@ -63,46 +65,43 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - const [applyingUriRef, setApplyingUri_] = useRefState(applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null) - const [streamStateRef, setStreamState_] = useRefState(editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null })) + const [_, rerender] = useState(0) - const setApplyingUri = useCallback((uri: URI | null) => { // switched the box's URI to whatever they clicked on most recently - setApplyingUri_(uri) - const newStreamState = editCodeService.getURIStreamState({ uri }) - if (uri) applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri - setStreamState_(newStreamState) - }, [applyBoxId, setApplyingUri_, editCodeService, setStreamState_]) + const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId]) + const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri]) // listen for stream updates useURIStreamState( - useCallback((uri, streamState) => { - const shouldUpdate = applyingUriRef.current?.fsPath === uri.fsPath - if (!shouldUpdate) return - setStreamState_(streamState) // editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null }) - }, [applyingUriRef, setStreamState_]) + useCallback((uri, newStreamState) => { + const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath + if (shouldUpdate) return + rerender(c => c + 1) + if (newStreamState !== streamState()) console.log('AAAAAAAAAAAAAAAAAAA') + }, [applyBoxId, editCodeService, applyingUri, rerender, streamState]) ) const onSubmit = useCallback(() => { if (isDisabled) return - if (streamStateRef.current === 'streaming') return - const uri = editCodeService.startApplying({ + if (streamState() === 'streaming') return + const newApplyingUri = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, - chatApplyBoxId: applyBoxId, }) - setApplyingUri(uri) + applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined + rerender(c => c + 1) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [editCodeService, applyBoxId, codeStr, metricsService, isDisabled, streamStateRef, setApplyingUri]) + }, [isDisabled, streamState, editCodeService, codeStr, applyBoxId, metricsService]) const onInterrupt = useCallback(() => { - if (streamStateRef.current !== 'streaming') return - if (!applyingUriRef.current) return + if (streamState() !== 'streaming') return + const uri = applyingUri() + if (!uri) return - editCodeService.interruptURIStreaming({ uri: applyingUriRef.current, }) + editCodeService.interruptURIStreaming({ uri }) metricsService.capture('Stop Apply', {}) - }, [editCodeService, metricsService, streamStateRef, applyingUriRef]) + }, [streamState, applyingUri,editCodeService, metricsService]) const isSingleLine = !codeStr.includes('\n') @@ -128,8 +127,8 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`} onClick={() => { - if (!applyingUriRef.current) return - editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'accept', removeCtrlKs: false }) + const uri = applyingUri() + if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) }} > Accept @@ -138,20 +137,21 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`} onClick={() => { - if (!applyingUriRef.current) return - editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'reject', removeCtrlKs: false }) + const uri = applyingUri() + if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) }} > Reject - console.log('streamStateRef.current', streamStateRef.current) + console.log('streamStateRef.current', streamState()) + const currStreamState = streamState() return <> - {streamStateRef.current !== 'streaming' && } - {streamStateRef.current === 'idle' && !isDisabled && applyButton} - {streamStateRef.current === 'streaming' && stopButton} - {streamStateRef.current === 'acceptRejectAll' && acceptRejectButtons} + {currStreamState !== 'streaming' && } + {currStreamState === 'idle' && !isDisabled && applyButton} + {currStreamState === 'streaming' && stopButton} + {currStreamState === 'acceptRejectAll' && acceptRejectButtons} } 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 1fbcc303..fe70caa3 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 @@ -65,7 +65,6 @@ export const QuickEditChat = ({ from: 'QuickEdit', type: 'rewrite', diffareaid, - chatApplyBoxId: null, }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) From 96de4f028f7fee8ff66571d16aa742b4d1d33344 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 04:35:56 -0800 Subject: [PATCH 76/92] remove unnecessary check --- .../browser/react/src/markdown/ApplyBlockHoverButtons.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 8c9f26d1..b31bfb7b 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 @@ -76,8 +76,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath if (shouldUpdate) return rerender(c => c + 1) - if (newStreamState !== streamState()) console.log('AAAAAAAAAAAAAAAAAAA') - }, [applyBoxId, editCodeService, applyingUri, rerender, streamState]) + }, [applyBoxId, editCodeService, applyingUri]) ) const onSubmit = useCallback(() => { @@ -101,7 +100,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin editCodeService.interruptURIStreaming({ uri }) metricsService.capture('Stop Apply', {}) - }, [streamState, applyingUri,editCodeService, metricsService]) + }, [streamState, applyingUri, editCodeService, metricsService]) const isSingleLine = !codeStr.includes('\n') From f40acd76f100f35985d448c1c01565ff7f3503f9 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 17:52:28 -0800 Subject: [PATCH 77/92] endpoint --- .../contrib/void/electron-main/llmMessage/sendLLMMessage.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 0a182aec..b3a12eb6 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -35,6 +35,8 @@ export const sendLLMMessage = ({ metricsService.capture(eventId, { providerName, modelName, + customEndpointURL: settingsOfProvider[providerName].endpoint, + numModelsAtEndpoint: settingsOfProvider[providerName].models.length, ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), From dd24c3180d5e2a2cc868cde1c299190be34433a2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 17:52:58 -0800 Subject: [PATCH 78/92] + --- .../contrib/void/electron-main/llmMessage/sendLLMMessage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index b3a12eb6..3e07a8f5 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -35,8 +35,8 @@ export const sendLLMMessage = ({ metricsService.capture(eventId, { providerName, modelName, - customEndpointURL: settingsOfProvider[providerName].endpoint, - numModelsAtEndpoint: settingsOfProvider[providerName].models.length, + customEndpointURL: settingsOfProvider[providerName]?.endpoint, + numModelsAtEndpoint: settingsOfProvider[providerName].models?.length, ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), From bf4392a52f1e65fa97082fd338583fe36d429750 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 17:53:14 -0800 Subject: [PATCH 79/92] safe --- .../contrib/void/electron-main/llmMessage/sendLLMMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 3e07a8f5..7bb4e141 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -36,7 +36,7 @@ export const sendLLMMessage = ({ providerName, modelName, customEndpointURL: settingsOfProvider[providerName]?.endpoint, - numModelsAtEndpoint: settingsOfProvider[providerName].models?.length, + numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length, ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), From 2a876d8efe43558a42eee73c56f57886c1689c10 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 21:08:34 -0800 Subject: [PATCH 80/92] + --- .../void/browser/react/src/markdown/ChatMarkdownRender.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 320dccbb..8168bed3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -9,7 +9,6 @@ import { BlockCode } from './BlockCode.js' import { ChatMessageLocation, } from '../../../aiRegexService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' -import { URI } from '../../../../../../../base/common/uri.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } From bdb897d03271dac674d9fae8d1579ee23103cc70 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Feb 2025 01:01:00 -0800 Subject: [PATCH 81/92] tool UI draft --- .../contrib/void/browser/chatThreadService.ts | 8 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 153 ++++++++++++++- .../void/browser/react/tailwind.config.js | 7 + .../contrib/void/common/toolsService.ts | 178 +++++++++++------- 4 files changed, 269 insertions(+), 77 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index e6a52c54..d92fb772 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -48,13 +48,13 @@ export type FileSelection = { export type StagingSelectionItem = CodeSelection | FileSelection -type ToolMessage = { +export type ToolMessage = { role: 'tool'; name: T; // internal use params: string; // internal use id: string; // apis require this tool use id content: string; // result - result: ToolCallReturnType; // text message of result + result: ToolCallReturnType[T]; // text message of result } @@ -430,10 +430,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 1. let toolResult: Awaited> - let toolResultVal: ToolCallReturnType + let toolResultVal: ToolCallReturnType[ToolName] try { toolResult = await this._toolsService.toolFns[toolName](tool.params) - toolResultVal = toolResult[0] + toolResultVal = toolResult } catch (error) { this._setStreamState(threadId, { error }) shouldSendAnotherMessage = false 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 3c95f434..7a0563c3 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 @@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js'; -import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js'; +import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; @@ -21,10 +21,11 @@ import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; -import { Pencil, X } from 'lucide-react'; +import { ChevronRight, Pencil, X } from 'lucide-react'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { ChatMessageLocation } from '../../../aiRegexService.js'; +import { ToolCallReturnType, ToolName } from '../../../../common/toolsService.js'; @@ -542,6 +543,146 @@ export const SelectedFiles = ( } +type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } +interface ToolResultProps { + title: string; + desc: string; + desc2?: number; + children?: React.ReactNode; +} + +const ToolResult = ({ + title, + desc, + desc2, + children, +}: ToolResultProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + const isDropdown = !!children + + return ( +
    +
    +
    children && setIsExpanded(!isExpanded)} + > + {isDropdown && ( + + )} +
    + {title} + {`"`}{desc}{`"`} + {desc2 !== undefined && ( + + {`(`}{desc2}{` result`}{desc2 !== 1 ? 's' : ''}{`)`} + + )} +
    +
    +
    + {children} +
    +
    +
    + ); +}; + + + +const toolResultToComponent: ToolReusltToComponent = { + 'read_file': ({ message }) => ( + + ), + 'list_dir': ({ message }) => ( + +
    + {message.result.children?.map((item, i) => ( +
    + • {item.name} + {item.isDirectory && '/'} +
    + ))} + {message.result.hasNextPage && ( +
    + {message.result.itemsRemaining} more items... +
    + )} +
    +
    + ), + 'pathname_search': ({ message }) => ( + +
    + {Array.isArray(message.result.uris) ? + message.result.uris.map((uri, i) => ( + + )) : +
    {message.result.uris}
    + } + {message.result.hasNextPage && ( +
    + More results available... +
    + )} +
    +
    + ), + 'search': ({ message }) => ( + +
    + {typeof message.result.uris === 'string' ? + message.result.uris : + message.result.uris.map((uri, i) => ( + + )) + } + {message.result.hasNextPage && ( +
    + More results available... +
    + )} +
    +
    + ) +}; + + + type ChatBubbleMode = 'display' | 'edit' const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => { @@ -695,7 +836,13 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatbubbleContents = } else if (role === 'tool') { - chatbubbleContents = chatMessage.name + + const ToolComponent = toolResultToComponent[chatMessage.name] as ({ message }: { message: any }) => React.ReactNode // ts isnt smart enough to deal with the types here... + + chatbubbleContents = + + console.log('tool result:', chatMessage.name, chatMessage.params, chatMessage.result) + } return
    = keyof typeof voidTools[T]['params'] export type ToolParamsObj = { [paramName in ToolParamNames]: unknown } +export type ToolCallReturnType = { + 'read_file': { uri: URI, fileContents: string, hasNextPage: boolean }, + 'list_dir': { rootURI: URI, children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, + 'pathname_search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean }, + 'search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean } + 'create_file': {} +} -export type ToolCallReturnType - = T extends 'read_file' ? string - : T extends 'list_dir' ? string - : T extends 'pathname_search' ? string | URI[] - : T extends 'search' ? string | URI[] - : T extends 'create_file' ? string - : never +type DirectoryItem = { + name: string; + isDirectory: boolean; + isSymbolicLink: boolean; +} -export type ToolFns = { [T in ToolName]: (p: string) => Promise<[ToolCallReturnType, boolean]> } -export type ToolResultToString = { [T in ToolName]: (result: [ToolCallReturnType, boolean]) => string } +export type ToolFns = { [T in ToolName]: (p: string) => Promise } +export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType[T]) => string } // pagination info const MAX_FILE_CHARS_PAGE = 50_000 const MAX_CHILDREN_URIs_PAGE = 500 -const MAX_DEPTH = 1 -async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, pageNumber: number): Promise<[string, boolean]> { - let output = ''; - const indentation = (depth: number, isLast: boolean): string => { - if (depth === 0) return ''; - return `${'| '.repeat(depth - 1)}${isLast ? '└── ' : '├── '}`; - }; - - let hasNextPage = false - - async function traverseChildren(uri: URI, depth: number, isLast: boolean) { - const stat = await fileService.resolve(uri, { resolveMetadata: false }); - - // we might want to say where symlink links to - if (depth === 0 && pageNumber !== 1) - output += '' - else - output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`; - - // list children - const originalChildrenLength = stat.children?.length ?? 0 - const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1) - const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 // INCLUSIVE - const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; - - if (!stat.isDirectory) return; - - if (listChildren.length === 0) return - if (depth === MAX_DEPTH) return // right now MAX_DEPTH=1 to make pagination work nicely - - for (let i = 0; i < Math.min(listChildren.length, MAX_CHILDREN_URIs_PAGE); i++) { - await traverseChildren(listChildren[i].resource, depth + 1, i === listChildren.length - 1); - } - const nCutoffResults = (originalChildrenLength - 1) - toChildIdx - if (nCutoffResults >= 1) { - output += `${indentation(depth + 1, true)}(${nCutoffResults} results remaining...)\n` - hasNextPage = true - } +const computeDirectoryResult = async ( + fileService: IFileService, + rootURI: URI, + pageNumber: number = 1 +): Promise => { + const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); + if (!stat.isDirectory) { + return { rootURI, children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 }; } - await traverseChildren(rootURI, 0, false); + const originalChildrenLength = stat.children?.length ?? 0; + const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1); + const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE + const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; + + const children: DirectoryItem[] = listChildren.map(child => ({ + name: child.name, + isDirectory: child.isDirectory, + isSymbolicLink: child.isSymbolicLink || false + })); + + const hasNextPage = (originalChildrenLength - 1) > toChildIdx; + const hasPrevPage = pageNumber > 1; + const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1)); + + return { + rootURI, + children, + hasNextPage, + hasPrevPage, + itemsRemaining + }; +}; + +const directoryResultToString = (result: ToolCallReturnType['list_dir']): string => { + if (!result.children) { + return `Error: ${result.rootURI} is not a directory`; + } + + let output = ''; + const entries = result.children; + + if (!result.hasPrevPage) { + output += `${result.rootURI}\n`; + } + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const isLast = i === entries.length - 1 && !result.hasNextPage; + const prefix = isLast ? '└── ' : '├── '; + + output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`; + } + + if (result.hasNextPage) { + output += `└── (${result.itemsRemaining} results remaining...)\n`; + } + + return output; +}; + + - return [output, hasNextPage] -} const validateJSON = (s: string): { [s: string]: unknown } => { @@ -217,6 +241,8 @@ export class ToolsService implements IToolsService { this.toolFns = { read_file: async (s: string) => { + console.log('read_file') + const o = validateJSON(s) const { uri: uriStr, pageNumber: pageNumberUnknown } = o @@ -227,22 +253,30 @@ export class ToolsService implements IToolsService { const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 - let fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate + const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 - return [fileContents || '(empty)', hasNextPage] + + console.log('read_file result:', fileContents) + + + return { uri, fileContents, hasNextPage } }, list_dir: async (s: string) => { + console.log('list_dir') const o = validateJSON(s) const { uri: uriStr, pageNumber: pageNumberUnknown } = o const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - const [treeStr, hasNextPage] = await generateDirectoryTreeMd(fileService, uri, pageNumber) - return [treeStr, hasNextPage] + const dirResult = await computeDirectoryResult(fileService, uri, pageNumber) + console.log('list_dir result:', dirResult) + + return dirResult }, pathname_search: async (s: string) => { + console.log('pathname_search') const o = validateJSON(s) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o @@ -254,15 +288,18 @@ export class ToolsService implements IToolsService { const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1) const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 - const URIs = data.results + const uris = data.results .slice(fromIdx, toIdx + 1) // paginate .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 + console.log('pathname_search result:', uris) - return [URIs, hasNextPage] + return { queryStr, uris, hasNextPage } }, search: async (s: string) => { + console.log('search') + const o = validateJSON(s) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o @@ -274,35 +311,37 @@ export class ToolsService implements IToolsService { const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1) const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 - const URIs = data.results + const uris = data.results .slice(fromIdx, toIdx + 1) // paginate .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - return [URIs, hasNextPage] + console.log('search result:', uris) + + return { queryStr, uris, hasNextPage } }, } - const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : '' this.toolResultToString = { - read_file: ([fileContents, hasNextPage]) => { - return fileContents + nextPageStr(hasNextPage) + read_file: (result) => { + return nextPageStr(result.hasNextPage) }, - list_dir: ([dirTreeStr, hasNextPage]) => { - return dirTreeStr + nextPageStr(hasNextPage) + list_dir: (result) => { + const dirTreeStr = directoryResultToString(result) + return dirTreeStr + nextPageStr(result.hasNextPage) }, - pathname_search: ([URIs, hasNextPage]) => { - if (typeof URIs === 'string') return URIs - return URIs.map(uri => uri.fsPath).join('\n') + nextPageStr(hasNextPage) + pathname_search: (result) => { + if (typeof result.uris === 'string') return result.uris + return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, - search: ([URIs, hasNextPage]) => { - if (typeof URIs === 'string') return URIs - return URIs.map(uri => uri.fsPath).join('\n') + nextPageStr(hasNextPage) + search: (result) => { + if (typeof result.uris === 'string') return result.uris + return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, } @@ -314,4 +353,3 @@ export class ToolsService implements IToolsService { } registerSingleton(IToolsService, ToolsService, InstantiationType.Eager); - From a9b3cc146bf4ff62ce1d1d8d586729ce1baa715d Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Feb 2025 02:07:33 -0800 Subject: [PATCH 82/92] refactor --- .../react/src/sidebar-tsx/SidebarChat.tsx | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) 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 7a0563c3..23767833 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 @@ -545,16 +545,16 @@ export const SelectedFiles = ( type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } interface ToolResultProps { - title: string; - desc: string; - desc2?: number; + actionTitle: string; + actionParam: string; + actionNumResults?: number; children?: React.ReactNode; } const ToolResult = ({ - title, - desc, - desc2, + actionTitle, + actionParam, + actionNumResults, children, }: ToolResultProps) => { const [isExpanded, setIsExpanded] = useState(false); @@ -563,7 +563,7 @@ const ToolResult = ({ return (
    -
    +
    children && setIsExpanded(!isExpanded)} @@ -574,11 +574,11 @@ const ToolResult = ({ /> )}
    - {title} - {`"`}{desc}{`"`} - {desc2 !== undefined && ( + {actionTitle} + {`"`}{actionParam}{`"`} + {actionNumResults !== undefined && ( - {`(`}{desc2}{` result`}{desc2 !== 1 ? 's' : ''}{`)`} + {`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`} )}
    @@ -598,15 +598,15 @@ const ToolResult = ({ const toolResultToComponent: ToolReusltToComponent = { 'read_file': ({ message }) => ( ), 'list_dir': ({ message }) => (
    {message.result.children?.map((item, i) => ( @@ -625,9 +625,9 @@ const toolResultToComponent: ToolReusltToComponent = { ), 'pathname_search': ({ message }) => (
    {Array.isArray(message.result.uris) ? @@ -653,9 +653,9 @@ const toolResultToComponent: ToolReusltToComponent = { ), 'search': ({ message }) => (
    {typeof message.result.uris === 'string' ? From d96a9d5f6b3f4de26def58f48e1126cb0f3aa51e Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Feb 2025 02:51:33 -0800 Subject: [PATCH 83/92] fix typeerror --- .../react/src/sidebar-tsx/SidebarThreadSelector.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 72dc6d39..6da3d5b9 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 @@ -68,13 +68,14 @@ export const SidebarThreadSelector = () => { let firstMsg = null; // let secondMsg = null; - const firstMsgIdx = pastThread.messages.findIndex( - (msg) => msg.role !== 'system' && !!msg.displayContent + const firstUserMsgIdx = pastThread.messages.findIndex( + (msg) => msg.role !== 'system' && msg.role !== 'tool' && !!msg.displayContent ); - if (firstMsgIdx !== -1) { + if (firstUserMsgIdx !== -1) { // firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? ''); - firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? ''; + const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx] + firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || ''; } else { firstMsg = '""'; } From c650091418af8ab3cce23c1991ea9d9d712db9ab Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Feb 2025 21:10:39 -0800 Subject: [PATCH 84/92] style --- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 12 ++++++------ .../contrib/void/browser/react/src/util/inputs.tsx | 2 +- .../contrib/void/browser/react/tailwind.config.js | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) 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 23767833..96e22bd4 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 @@ -610,8 +610,8 @@ const toolResultToComponent: ToolReusltToComponent = { >
    {message.result.children?.map((item, i) => ( -
    - • {item.name} +
    + {item.name} {item.isDirectory && '/'}
    ))} @@ -632,12 +632,12 @@ const toolResultToComponent: ToolReusltToComponent = {
    {Array.isArray(message.result.uris) ? message.result.uris.map((uri, i) => ( -
    + )) : @@ -661,12 +661,12 @@ const toolResultToComponent: ToolReusltToComponent = { {typeof message.result.uris === 'string' ? message.result.uris : message.result.uris.map((uri, i) => ( -
    + )) 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 270870a7..be327655 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 @@ -713,7 +713,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars onCreateInstance={useCallback((editor: CodeEditorWidget) => { const model = modelOfEditorId[id] ?? modelService.createModel( - initValueRef.current, { + initValueRef.current + '\n', { languageId: languageRef.current ? languageRef.current : 'typescript', onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this }) diff --git a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js index f9fcaef6..bc57116b 100644 --- a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js @@ -28,6 +28,7 @@ module.exports = { colors: { "void-bg-1": "var(--vscode-input-background)", + "void-bg-1-alt": "var(--vscode-badge-background)", "void-bg-2": "var(--vscode-sideBar-background)", "void-bg-2-alt": "color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%)", "void-bg-3": "var(--vscode-editor-background)", From 2c2714273effb7b59be63e58271f5dfb17e1402d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 23 Feb 2025 01:28:48 -0800 Subject: [PATCH 85/92] partway through adding better support for more providers --- .vscode/settings.json | 2 +- .../void/browser/autocompleteService.ts | 33 +- .../contrib/void/common/llmMessageService.ts | 2 + .../contrib/void/common/llmMessageTypes.ts | 1 + .../contrib/void/common/voidSettingsTypes.ts | 435 +-------------- .../void/electron-main/llmMessage/MODELS.ts | 509 ++++++++++++++++++ .../void/electron-main/llmMessage/_old.ts | 387 +++++++++++++ .../electron-main/llmMessage/anthropic.ts | 114 ---- .../void/electron-main/llmMessage/ollama.ts | 124 ----- .../void/electron-main/llmMessage/openai.ts | 231 -------- .../llmMessage/postprocessToolCalls.ts | 8 - .../llmMessage/preprocessLLMMessages.ts | 418 +++++++------- .../llmMessage/sendLLMMessage.ts | 14 +- .../void/electron-main/llmMessageChannel.ts | 3 +- 14 files changed, 1173 insertions(+), 1108 deletions(-) create mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 910e0bec..d24ab6e5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -113,7 +113,7 @@ "files.insertFinalNewline": false }, "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.defaultFormatter": "ms-vsliveshare.vsliveshare", "editor.formatOnSave": true }, "[javascript]": { diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 5fc8ac76..f9cbec7b 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -795,26 +795,27 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ }, useProviderFor: 'Autocomplete', logging: { loggingName: 'Autocomplete' }, - onText: async ({ fullText, newText }) => { + onText: () => { }, // unused in FIMMessage + // onText: async ({ fullText, newText }) => { - newAutocompletion.insertText = fullText + // newAutocompletion.insertText = fullText - // count newlines in newText - const numNewlines = newText.match(/\n|\r\n/g)?.length || 0 - newAutocompletion._newlineCount += numNewlines + // // count newlines in newText + // const numNewlines = newText.match(/\n|\r\n/g)?.length || 0 + // newAutocompletion._newlineCount += numNewlines - // if too many newlines, resolve up to last newline - if (newAutocompletion._newlineCount > 10) { - const lastNewlinePos = fullText.lastIndexOf('\n') - newAutocompletion.insertText = fullText.substring(0, lastNewlinePos) - resolve(newAutocompletion.insertText) - return - } + // // if too many newlines, resolve up to last newline + // if (newAutocompletion._newlineCount > 10) { + // const lastNewlinePos = fullText.lastIndexOf('\n') + // newAutocompletion.insertText = fullText.substring(0, lastNewlinePos) + // resolve(newAutocompletion.insertText) + // return + // } - // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { - // reject('LLM response did not match user\'s text.') - // } - }, + // // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { + // // reject('LLM response did not match user\'s text.') + // // } + // }, onFinalMessage: ({ fullText }) => { // console.log('____res: ', JSON.stringify(newAutocompletion.insertText)) diff --git a/src/vs/workbench/contrib/void/common/llmMessageService.ts b/src/vs/workbench/contrib/void/common/llmMessageService.ts index bb6cf09c..c8f9ea2c 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageService.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageService.ts @@ -157,6 +157,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this.channel.call('ollamaList', { ...proxyParams, settingsOfProvider, + providerName: 'ollama', requestId: requestId_, } satisfies MainModelListParams) } @@ -175,6 +176,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this.channel.call('openAICompatibleList', { ...proxyParams, settingsOfProvider, + providerName: 'openAICompatible', requestId: requestId_, } satisfies MainModelListParams) } diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 0956b08b..58989cce 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -191,6 +191,7 @@ export type OpenaiCompatibleModelResponse = { // params to the true list fn export type ModelListParams = { + providerName: ProviderName; settingsOfProvider: SettingsOfProvider; onSuccess: (param: { models: modelResponse[] }) => void; onError: (param: { error: string }) => void; diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 529a872c..fb387bc1 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -4,367 +4,13 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { defaultModelsOfProvider } from '../electron-main/llmMessage/MODELS.js'; import { VoidSettingsState } from './voidSettingsService.js' - -// developer info used in sendLLMMessage -export type DeveloperInfoAtModel = { - // USED: - supportsSystemMessage: 'developer' | boolean, // if null, we will just do a string of system message. this is independent from separateSystemMessage, which takes priority and is passed directly in each provider's implementation. - supportsTools: boolean, // we will just do a string of tool use if it doesn't support - - // UNUSED (coming soon): - // TODO!!! think tokens - deepseek - _recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized - _supportsStreaming: boolean, // we will just dump the final result if doesn't support it - _supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> - _maxTokens: number, // required -} - -export type DeveloperInfoAtProvider = { - overrideSettingsForAllModels?: Partial; // any overrides for models that a provider might have (e.g. if a provider always supports tool use, even if we don't recognize the model we can set tools to true) -} - - - - - -export type VoidModelInfo = { // <-- STATEFUL - modelName: string, - isDefault: boolean, // whether or not it's a default for its provider - isHidden: boolean, // whether or not the user is hiding it (switched off) - isAutodetected?: boolean, // whether the model was autodetected by polling -} & DeveloperInfoAtModel - - - - - -export const recognizedModels = [ - // chat - 'OpenAI 4o', - 'Anthropic Claude', - 'Llama 3.x', - 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model - 'xAI Grok', - // 'xAI Grok', - // 'Google Gemini, Gemma', - // 'Microsoft Phi4', - - - // coding (autocomplete) - 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 - 'Mistral Codestral', - - // thinking - 'OpenAI o1', - 'Deepseek R1', - - // general - // 'Mixtral 8x7b' - // 'Qwen2.5', - -] as const - -type RecognizedModelName = (typeof recognizedModels)[number] | '' - - -export function recognizedModelOfModelName(modelName: string): RecognizedModelName { - const lower = modelName.toLowerCase(); - - if (lower.includes('gpt-4o')) - return 'OpenAI 4o'; - if (lower.includes('claude')) - return 'Anthropic Claude'; - if (lower.includes('llama')) - return 'Llama 3.x'; - if (lower.includes('qwen2.5-coder')) - return 'Alibaba Qwen2.5 Coder Instruct'; - if (lower.includes('mistral')) - return 'Mistral Codestral'; - if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 - return 'OpenAI o1'; - if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) - return 'Deepseek R1'; - if (lower.includes('deepseek')) - return 'Deepseek Chat' - if (lower.includes('grok')) - return 'xAI Grok' - - return ''; -} - - -const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { - 'anthropic': { - overrideSettingsForAllModels: { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: true, - } - }, - 'deepseek': { - overrideSettingsForAllModels: { - } - }, - 'ollama': { - }, - 'openRouter': { - }, - 'openAICompatible': { - }, - 'openAI': { - }, - 'gemini': { - }, - 'mistral': { - }, - 'groq': { - }, - 'xAI': { - }, - 'vLLM': { - }, -} -export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { - return developerInfoAtProvider[providerName] ?? {} -} - - - - -// providerName is optional, but gives some extra fallbacks if provided -const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { - 'OpenAI 4o': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: true, - _maxTokens: 4096, - }, - - 'Anthropic Claude': { - supportsSystemMessage: true, - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'Llama 3.x': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'xAI Grok': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: true, - _maxTokens: 4096, - - }, - - 'Deepseek Chat': { - supportsSystemMessage: true, - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'Alibaba Qwen2.5 Coder Instruct': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'Mistral Codestral': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'OpenAI o1': { - supportsSystemMessage: 'developer', - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: true, - _maxTokens: 4096, - }, - - 'Deepseek R1': { - supportsSystemMessage: false, - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - - '': { - supportsSystemMessage: false, - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, -} -export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { - const recognizedModelName = recognizedModelOfModelName(modelName) - return { - _recognizedModelName: recognizedModelName, - ...developerInfoOfRecognizedModelName[recognizedModelName], - ...overrides - } -} - - - - - - -// creates `modelInfo` from `modelNames` -export const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): VoidModelInfo[] => { - return defaultModelNames.map((modelName, i) => ({ - modelName, - isDefault: true, - isAutodetected: false, - isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually - ...developerInfoOfModelName(modelName), - })) -} - -export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { - const { existingModels } = options - - const existingModelsMap: Record = {} - for (const existingModel of existingModels) { - existingModelsMap[existingModel.modelName] = existingModel - } - - return defaultModelNames.map((modelName, i) => ({ - modelName, - isDefault: true, - isAutodetected: true, - isHidden: !!existingModelsMap[modelName]?.isHidden, - ...developerInfoOfModelName(modelName) - })) -} - - - - - -// https://docs.anthropic.com/en/docs/about-claude/models -export const defaultAnthropicModels = modelInfoOfDefaultModelNames([ - 'claude-3-5-sonnet-20241022', - 'claude-3-5-haiku-20241022', - 'claude-3-opus-20240229', - 'claude-3-sonnet-20240229', - // 'claude-3-haiku-20240307', -]) - - -// https://platform.openai.com/docs/models/gp -export const defaultOpenAIModels = modelInfoOfDefaultModelNames([ - 'o1', - 'o1-mini', - 'o3-mini', - 'gpt-4o', - 'gpt-4o-mini', - // 'gpt-4o-2024-05-13', - // 'gpt-4o-2024-08-06', - // 'gpt-4o-mini-2024-07-18', - // 'gpt-4-turbo', - // 'gpt-4-turbo-2024-04-09', - // 'gpt-4-turbo-preview', - // 'gpt-4-0125-preview', - // 'gpt-4-1106-preview', - // 'gpt-4', - // 'gpt-4-0613', - // 'gpt-3.5-turbo-0125', - // 'gpt-3.5-turbo', - // 'gpt-3.5-turbo-1106', -]) - -// https://platform.openai.com/docs/models/gp -export const defaultDeepseekModels = modelInfoOfDefaultModelNames([ - 'deepseek-chat', - 'deepseek-reasoner', -]) - - -// https://console.groq.com/docs/models -export const defaultGroqModels = modelInfoOfDefaultModelNames([ - "llama3-70b-8192", - "llama-3.3-70b-versatile", - "llama-3.1-8b-instant", - "gemma2-9b-it", - "mixtral-8x7b-32768" -]) - - -export const defaultGeminiModels = modelInfoOfDefaultModelNames([ - 'gemini-1.5-flash', - 'gemini-1.5-pro', - 'gemini-1.5-flash-8b', - 'gemini-2.0-flash-exp', - 'gemini-2.0-flash-thinking-exp-1219', - 'learnlm-1.5-pro-experimental' -]) - -export const defaultMistralModels = modelInfoOfDefaultModelNames([ - "codestral-latest", - "open-codestral-mamba", - "open-mistral-nemo", - "mistral-large-latest", - "pixtral-large-latest", - "ministral-3b-latest", - "ministral-8b-latest", - "mistral-small-latest", -]) - -export const defaultXAIModels = modelInfoOfDefaultModelNames([ - 'grok-2-latest', - 'grok-3-latest', -]) -// export const parseMaxTokensStr = (maxTokensStr: string) => { -// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN -// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) -// if (Number.isNaN(int)) -// return undefined -// return int -// } - - - - -export const anthropicMaxPossibleTokens = (modelName: string) => { - if (modelName === 'claude-3-5-sonnet-20241022' - || modelName === 'claude-3-5-haiku-20241022') - return 8192 - if (modelName === 'claude-3-opus-20240229' - || modelName === 'claude-3-sonnet-20240229' - || modelName === 'claude-3-haiku-20240307') - return 4096 - return 1024 // return a reasonably small number if they're using a different model -} - - type UnionOfKeys = T extends T ? keyof T : never; - export const defaultProviderSettings = { anthropic: { apiKey: '', @@ -418,6 +64,14 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => { +export type VoidModelInfo = { // <-- STATEFUL + modelName: string, + isDefault: boolean, // whether or not it's a default for its provider + isHidden: boolean, // whether or not the user is hiding it (switched off) + isAutodetected?: boolean, // whether the model was autodetected by polling +} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves + + type CommonProviderSettings = { _didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields @@ -434,10 +88,6 @@ export type SettingsOfProvider = { export type SettingName = keyof SettingsAtProvider - - - - type DisplayInfoForProviderName = { title: string, desc?: string, @@ -584,110 +234,83 @@ const defaultCustomSettings: Record = { } - -export const voidInitModelOptions = { - anthropic: { - models: defaultAnthropicModels, - }, - openAI: { - models: defaultOpenAIModels, - }, - deepseek: { - models: defaultDeepseekModels, - }, - ollama: { - models: [], - }, - vLLM: { - models: [], - }, - openRouter: { - models: [], // any string - }, - openAICompatible: { - models: [], - }, - gemini: { - models: defaultGeminiModels, - }, - groq: { - models: defaultGroqModels, - }, - mistral: { - models: defaultMistralModels, - }, - xAI: { - models: defaultXAIModels, +const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidModelInfo[] } => { + return { + models: defaultModelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: false, + isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually + })) } -} satisfies Record - +} // used when waiting and for a type reference export const defaultSettingsOfProvider: SettingsOfProvider = { anthropic: { ...defaultCustomSettings, ...defaultProviderSettings.anthropic, - ...voidInitModelOptions.anthropic, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.anthropic), _didFillInProviderSettings: undefined, }, openAI: { ...defaultCustomSettings, ...defaultProviderSettings.openAI, - ...voidInitModelOptions.openAI, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAI), _didFillInProviderSettings: undefined, }, deepseek: { ...defaultCustomSettings, ...defaultProviderSettings.deepseek, - ...voidInitModelOptions.deepseek, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.deepseek), _didFillInProviderSettings: undefined, }, gemini: { ...defaultCustomSettings, ...defaultProviderSettings.gemini, - ...voidInitModelOptions.gemini, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.gemini), _didFillInProviderSettings: undefined, }, mistral: { ...defaultCustomSettings, ...defaultProviderSettings.mistral, - ...voidInitModelOptions.mistral, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral), _didFillInProviderSettings: undefined, }, xAI: { ...defaultCustomSettings, ...defaultProviderSettings.xAI, - ...voidInitModelOptions.xAI, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.xAI), _didFillInProviderSettings: undefined, }, groq: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.groq, - ...voidInitModelOptions.groq, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.groq), _didFillInProviderSettings: undefined, }, openRouter: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.openRouter, - ...voidInitModelOptions.openRouter, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openRouter), _didFillInProviderSettings: undefined, }, openAICompatible: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.openAICompatible, - ...voidInitModelOptions.openAICompatible, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAICompatible), _didFillInProviderSettings: undefined, }, ollama: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.ollama, - ...voidInitModelOptions.ollama, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.ollama), _didFillInProviderSettings: undefined, }, vLLM: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.vLLM, - ...voidInitModelOptions.vLLM, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM), _didFillInProviderSettings: undefined, }, } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts new file mode 100644 index 00000000..3cefdfa9 --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -0,0 +1,509 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import OpenAI from 'openai'; +import { Model as OpenAIModel } from 'openai/resources/models.js'; +import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; +import { InternalToolInfo, ToolName, toolNames } from '../../common/toolsService.js'; +import { defaultProviderSettings, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +import { prepareMessages } from './preprocessLLMMessages.js'; +import Anthropic from '@anthropic-ai/sdk'; +import { Ollama } from 'ollama'; + + + +export const defaultModelsOfProvider = { + anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models + 'claude-3-5-sonnet-latest', + 'claude-3-5-haiku-latest', + 'claude-3-opus-latest', + ], + openAI: [ // https://platform.openai.com/docs/models/gp + 'o1', + 'o1-mini', + 'o3-mini', + 'gpt-4o', + 'gpt-4o-mini', + ], + deepseek: [ // https://platform.openai.com/docs/models/gp + 'deepseek-chat', + 'deepseek-reasoner', + ], + ollama: [], + vLLM: [], + openRouter: [], + openAICompatible: [], + gemini: [ + 'gemini-1.5-flash', + 'gemini-1.5-pro', + 'gemini-1.5-flash-8b', + 'gemini-2.0-flash-exp', + 'gemini-2.0-flash-thinking-exp-1219', + 'learnlm-1.5-pro-experimental' + ], + groq: [ // https://console.groq.com/docs/models + "llama3-70b-8192", + "llama-3.3-70b-versatile", + "llama-3.1-8b-instant", + "gemma2-9b-it", + "mixtral-8x7b-32768" + ], + mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ + "codestral-latest", + "open-codestral-mamba", + "open-mistral-nemo", + "mistral-large-latest", + "pixtral-large-latest", + "ministral-3b-latest", + "ministral-8b-latest", + "mistral-small-latest", + ], + xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1 + 'grok-3-latest', + 'grok-2-latest', + ], +} satisfies Record + + + +type ProviderSettings = { + thinkingFormat: string; + toolsFormat: string; + FIMFormat: string; + modelOptions: { + [key: string]: { + contextWindow: number; + cost: { + input: number; + output: number; + cache_read?: number; + cache_write?: number; + }; + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; + supportsTools: false | 'anthropic-style' | 'openai-style'; + }; + }; +}; + + +const openAIProviderSettings: ProviderSettings = { + + thinkingFormat: '', + + toolsFormat: '', + + FIMFormat: '', + + modelOptions: { + "o1": { + contextWindow: 128_000, + cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, + supportsTools: false, + supportsSystemMessage: 'developer-role', + }, + "o3-mini": { + contextWindow: 200_000, + cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + supportsTools: false, + supportsSystemMessage: 'developer-role', + }, + "gpt-4o": { + contextWindow: 128_000, + cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, + supportsTools: 'openai-style', + supportsSystemMessage: 'system-role', + }, + } + +} + + + + + +const anthropicProviderSettings: ProviderSettings = { + thinkingFormat: '', + + toolsFormat: '', + + FIMFormat: '', + + modelOptions: { + "claude-3-5-sonnet-20241022": { + contextWindow: 200_000, + cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + + }, + "claude-3-5-haiku-20241022": { + contextWindow: 200_000, + cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + }, + "claude-3-opus-20240229": { + contextWindow: 200_000, + cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + }, + "claude-3-sonnet-20240229": { + contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + } + } +} + + + +const grokProviderSettings: ProviderSettings = { + thinkingFormat: '', + + toolsFormat: '', + + FIMFormat: '', + + modelOptions: { + "claude-3-5-sonnet-20241022": { + contextWindow: 200_000, + cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + + }, + "claude-3-5-haiku-20241022": { + contextWindow: 200_000, + cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + }, + "claude-3-opus-20240229": { + contextWindow: 200_000, + cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + }, + "claude-3-sonnet-20240229": { + contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + } + } + +} + + + + +// helpers + +const toolNamesSet = new Set(toolNames) +const isAToolName = (toolName: string): toolName is ToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + +// ------------ OPENAI-COMPATIBLE (HELPERS) ------------ +const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { + const { name, description, params, required } = toolInfo + return { + type: 'function', + function: { + name: name, + description: description, + parameters: { + type: 'object', + properties: params, + required: required, + } + } + } satisfies OpenAI.Chat.Completions.ChatCompletionTool +} + +type ToolCallOfIndex = { [index: string]: { name: string, params: string, id: string } } + +const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex) => { + return Object.keys(toolCallOfIndex).map(index => { + const tool = toolCallOfIndex[index] + return isAToolName(tool.name) ? { name: tool.name, id: tool.id, params: tool.params } : null + }).filter(t => !!t) +} + + +const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName }) => { + if (providerName === 'openAI') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, }) + } + else if (providerName === 'ollama') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, }) + } + else if (providerName === 'vLLM') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, }) + } + else throw new Error(`Invalid providerName ${providerName}`) +} + +export const _sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: '', supportsTools: '', }) + const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined + + const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} + const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) + const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj } + + let fullText = '' + const toolCallOfIndex: ToolCallOfIndex = {} + openai.chat.completions + .create(options) + .then(async response => { + _setAborter(() => response.controller.abort()) + // when receive text + for await (const chunk of response) { + // tool call + for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) { + const index = tool.index + if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', params: '', id: '' } + toolCallOfIndex[index].name += tool.function?.name ?? '' + toolCallOfIndex[index].params += tool.function?.arguments ?? ''; + toolCallOfIndex[index].id = tool.id ?? '' + } + // message + let newText = '' + newText += chunk.choices[0]?.delta?.content ?? '' + fullText += newText + onText({ newText, fullText }) + } + onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) }); + }) + // when error/fail - this catches errors of both .create() and .then(for await) + .catch(error => { + if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }); } + else { onError({ message: error + '', fullError: error }); } + }) +} + + +export const _openaiCompatibleList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }) => { + const onSuccess = ({ models }: { models: OpenAIModel[] }) => { + onSuccess_({ models }) + } + const onError = ({ error }: { error: string }) => { + onError_({ error }) + } + try { + const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) + openai.models.list() + .then(async (response) => { + const models: OpenAIModel[] = [] + models.push(...response.data) + while (response.hasNextPage()) { + models.push(...(await response.getNextPage()).data) + } + onSuccess({ models }) + }) + .catch((error) => { + onError({ error: error + '' }) + }) + } + catch (error) { + onError({ error: error + '' }) + } +} + + +// ------------ OPENAI ------------ +export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = (params) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ ANTHROPIC ------------ +const toAnthropicTool = (toolInfo: InternalToolInfo) => { + const { name, description, params, required } = toolInfo + return { + name: name, + description: description, + input_schema: { + type: 'object', + properties: params, + required: required, + } + } satisfies Anthropic.Messages.Tool +} + +const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[]) => { + return content.map(c => { + if (c.type !== 'tool_use') return null + if (!isAToolName(c.name)) return null + return c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null + }).filter(t => !!t) +} + +export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => { + const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', }) + + const thisConfig = settingsOfProvider.anthropic + const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); + const tools = ((tools_?.length ?? 0) !== 0) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined + + const maxTokens = ; + const stream = anthropic.messages.stream({ + system: separateSystemMessageStr, + messages: messages, + model: modelName, + max_tokens: maxTokens, + tools: tools, + tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time + }) + // when receive text + stream.on('text', (newText, fullText) => { + onText({ newText, fullText }) + }) + // when we get the final message on this stream (or when error/fail) + stream.on('finalMessage', (response) => { + const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') + const toolCalls = toolCallsFromAnthropicContent(response.content) + onFinalMessage({ fullText: content, toolCalls }) + }) + // on error + stream.on('error', (error) => { + if (error instanceof Anthropic.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }) } + else { onError({ message: error + '', fullError: error }) } + }) + _setAborter(() => stream.controller.abort()) +}; + +// // in future, can do tool_use streaming in anthropic, but it's pretty fast even without streaming... +// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} +// stream.on('streamEvent', e => { +// if (e.type === 'content_block_start') { +// if (e.content_block.type !== 'tool_use') return +// const index = e.index +// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' } +// toolCallOfIndex[index].name += e.content_block.name ?? '' +// toolCallOfIndex[index].args += e.content_block.input ?? '' +// } +// else if (e.type === 'content_block_delta') { +// if (e.delta.type !== 'input_json_delta') return +// toolCallOfIndex[e.index].args += e.delta.partial_json +// } +// }) + + +// ------------ OLLAMA ------------ +const newOllamaSDK = ({ endpoint }: { endpoint: string }) => { + // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in + if (!endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`) + const ollama = new Ollama({ host: endpoint }) + return ollama +} + +export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { + const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => { + onSuccess_({ models }) + } + const onError = ({ error }: { error: string }) => { + onError_({ error }) + } + try { + const thisConfig = settingsOfProvider.ollama + const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) + ollama.list() + .then((response) => { + const { models } = response + onSuccess({ models }) + }) + .catch((error) => { + onError({ error: error + '' }) + }) + } + catch (error) { + onError({ error: error + '' }) + } +} + +export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { + const thisConfig = settingsOfProvider.ollama + const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) + + let fullText = '' + ollama.generate({ + model: modelName, + prompt: messages.prefix, + suffix: messages.suffix, + options: { + stop: messages.stopTokens, + num_predict: 300, // max tokens + // repeat_penalty: 1, + }, + raw: true, + stream: true, // stream is not necessary but lets us expose the + }) + .then(async stream => { + _setAborter(() => stream.abort()) + for await (const chunk of stream) { + const newText = chunk.response + fullText += newText + } + onFinalMessage({ fullText }) + }) + // when error/fail + .catch((error) => { + onError({ message: error + '', fullError: error }) + }) +} + + +// ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat! +export const sendOllamaMessage: _InternalSendLLMChatMessageFnType = (params) => { + return _sendOpenAICompatibleChat(params) + // TODO!!! filter out reasoning tags... +} + + + +// ------------ OPENROUTER ------------ +export const sendOpenRouterFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { + // TODO!!! +} + +export const sendOpenRouterChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { + // payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models + // response.choices[0].delta.reasoning +} + +// ------------ OPENAI-COMPATIBLE ------------ +export const openAICompatibleList: _InternalModelListFnType = async (params) => { + return _openaiCompatibleList(params) +} + +// TODO!!! FIM + +// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration +export const sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = (params) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ VLLM ------------ + +// TODO!!! FIM + +// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration +export const sendVLLMChat: _InternalSendLLMChatMessageFnType = (params) => { + return _sendOpenAICompatibleChat(params) + // response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions +} + + + + + + + diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts index e1e90245..8aa80bf4 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts @@ -94,3 +94,390 @@ + + + + + + + + + + + + + + + + + + + +// export const recognizedModels = [ +// // chat +// 'OpenAI 4o', +// 'Anthropic Claude', +// 'Llama 3.x', +// 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model +// 'xAI Grok', +// // 'xAI Grok', +// // 'Google Gemini, Gemma', +// // 'Microsoft Phi4', + + +// // coding (autocomplete) +// 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 +// 'Mistral Codestral', + +// // thinking +// 'OpenAI o1', +// 'Deepseek R1', + +// // general +// // 'Mixtral 8x7b' +// // 'Qwen2.5', + +// ] as const + +// type RecognizedModelName = (typeof recognizedModels)[number] | '' + + +// export function recognizedModelOfModelName(modelName: string): RecognizedModelName { +// const lower = modelName.toLowerCase(); + +// if (lower.includes('gpt-4o')) +// return 'OpenAI 4o'; +// if (lower.includes('claude')) +// return 'Anthropic Claude'; +// if (lower.includes('llama')) +// return 'Llama 3.x'; +// if (lower.includes('qwen2.5-coder')) +// return 'Alibaba Qwen2.5 Coder Instruct'; +// if (lower.includes('mistral')) +// return 'Mistral Codestral'; +// if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 +// return 'OpenAI o1'; +// if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) +// return 'Deepseek R1'; +// if (lower.includes('deepseek')) +// return 'Deepseek Chat' +// if (lower.includes('grok')) +// return 'xAI Grok' + +// return ''; +// } + + +// const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { +// 'anthropic': { +// overrideSettingsForAllModels: { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: true, +// } +// }, +// 'deepseek': { +// overrideSettingsForAllModels: { +// } +// }, +// 'ollama': { +// }, +// 'openRouter': { +// }, +// 'openAICompatible': { +// }, +// 'openAI': { +// }, +// 'gemini': { +// }, +// 'mistral': { +// }, +// 'groq': { +// }, +// 'xAI': { +// }, +// 'vLLM': { +// }, +// } +// export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { +// return developerInfoAtProvider[providerName] ?? {} +// } + + + + +// // providerName is optional, but gives some extra fallbacks if provided +// const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { +// 'OpenAI 4o': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: true, +// _maxTokens: 4096, +// }, + +// 'Anthropic Claude': { +// supportsSystemMessage: true, +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'Llama 3.x': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'xAI Grok': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: true, +// _maxTokens: 4096, + +// }, + +// 'Deepseek Chat': { +// supportsSystemMessage: true, +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'Alibaba Qwen2.5 Coder Instruct': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'Mistral Codestral': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'OpenAI o1': { +// supportsSystemMessage: 'developer', +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: true, +// _maxTokens: 4096, +// }, + +// 'Deepseek R1': { +// supportsSystemMessage: false, +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + + +// '': { +// supportsSystemMessage: false, +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, +// } +// export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { +// const recognizedModelName = recognizedModelOfModelName(modelName) +// return { +// _recognizedModelName: recognizedModelName, +// ...developerInfoOfRecognizedModelName[recognizedModelName], +// ...overrides +// } +// } + + + + + + +// // creates `modelInfo` from `modelNames` + + + + + +// export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { +// const { existingModels } = options + +// const existingModelsMap: Record = {} +// for (const existingModel of existingModels) { +// existingModelsMap[existingModel.modelName] = existingModel +// } + +// return defaultModelNames.map((modelName, i) => ({ +// modelName, +// isDefault: true, +// isAutodetected: true, +// isHidden: !!existingModelsMap[modelName]?.isHidden, +// ...developerInfoOfModelName(modelName) +// })) +// } + + + + + + +// export const anthropicMaxPossibleTokens = (modelName: string) => { +// if (modelName === 'claude-3-5-sonnet-20241022' +// || modelName === 'claude-3-5-haiku-20241022') +// return 8192 +// if (modelName === 'claude-3-opus-20240229' +// || modelName === 'claude-3-sonnet-20240229' +// || modelName === 'claude-3-haiku-20240307') +// return 4096 +// return 1024 // return a reasonably small number if they're using a different model +// } + + + + + + + + + + + + + + + + + + + +// // Ollama chat +// export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { + +// const thisConfig = settingsOfProvider.ollama +// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in +// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) + +// let fullText = '' + +// const ollama = new Ollama({ host: thisConfig.endpoint }) + +// ollama.chat({ +// model: modelName, +// messages: messages, +// stream: true, +// // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens +// }) +// .then(async stream => { +// _setAborter(() => stream.abort()) +// // iterate through the stream +// for await (const chunk of stream) { +// const newText = chunk.message.content; + +// // chunk.message.tool_calls[0].function.arguments + +// fullText += newText; +// onText({ newText, fullText }); +// } + +// onFinalMessage({ fullText, tools: [] }); + +// }) +// // when error/fail +// .catch((error) => { +// onError({ message: error + '', fullError: error }) +// }) + +// }; + + + + + + + +// type NewParams = Pick[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'> +// const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { + +// if (providerName === 'openAI') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true +// }) +// } +// else if (providerName === 'ollama') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'vLLM') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'openRouter') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// defaultHeaders: { +// 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. +// 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. +// }, +// }) +// } +// else if (providerName === 'gemini') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'deepseek') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'openAICompatible') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'mistral') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'groq') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'xAI') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else { +// console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`) +// throw new Error(`Void providerName was invalid: ${providerName}`) +// } +// } + + diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts deleted file mode 100644 index c4338ebb..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ /dev/null @@ -1,114 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import Anthropic from '@anthropic-ai/sdk'; -import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; -import { anthropicMaxPossibleTokens, developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; -import { InternalToolInfo } from '../../common/toolsService.js'; -import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; -import { isAToolName } from './postprocessToolCalls.js'; - - - - -export const toAnthropicTool = (toolInfo: InternalToolInfo) => { - const { name, description, params, required } = toolInfo - return { - name: name, - description: description, - input_schema: { - type: 'object', - properties: params, - required: required, - } - } satisfies Anthropic.Messages.Tool -} - - - - - -export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => { - - const thisConfig = settingsOfProvider.anthropic - - const maxTokens = anthropicMaxPossibleTokens(modelName) - if (maxTokens === undefined) { - onError({ message: `Please set a value for Max Tokens.`, fullError: null }) - return - } - - const { messages, separateSystemMessageStr } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: true }) - - const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const { supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) - - const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); - - const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined - - const stream = anthropic.messages.stream({ - system: separateSystemMessageStr, - messages: messages, - model: modelName, - max_tokens: maxTokens, - tools: tools, - tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time - }) - - - // when receive text - stream.on('text', (newText, fullText) => { - onText({ newText, fullText }) - }) - - - // // can do tool use streaming - // const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} - // stream.on('streamEvent', e => { - // if (e.type === 'content_block_start') { - // if (e.content_block.type !== 'tool_use') return - // const index = e.index - // if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' } - // toolCallOfIndex[index].name += e.content_block.name ?? '' - // toolCallOfIndex[index].args += e.content_block.input ?? '' - // } - // else if (e.type === 'content_block_delta') { - // if (e.delta.type !== 'input_json_delta') return - // toolCallOfIndex[e.index].args += e.delta.partial_json - // } - // // TODO!!!!! - // // onText({}) - // }) - - // when we get the final message on this stream (or when error/fail) - stream.on('finalMessage', (response) => { - // stringify the response's content - const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - const toolCalls = response.content - .map(c => { - if (c.type !== 'tool_use') return null - if (!isAToolName(c.name)) return null - return c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null - }) - .filter(t => !!t) - - onFinalMessage({ fullText: content, toolCalls }) - }) - - stream.on('error', (error) => { - // the most common error will be invalid API key (401), so we handle this with a nice message - if (error instanceof Anthropic.APIError && error.status === 401) { - onError({ message: 'Invalid API key.', fullError: error }) - } - else { - onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this - } - }) - - // TODO need to test this to make sure it works, it might throw an error - _setAborter(() => stream.controller.abort()) - -}; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts deleted file mode 100644 index da6715c0..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts +++ /dev/null @@ -1,124 +0,0 @@ - -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Ollama } from 'ollama'; -import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; -import { defaultProviderSettings } from '../../common/voidSettingsTypes.js'; - -export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { - - const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => { - onSuccess_({ models }) - } - - const onError = ({ error }: { error: string }) => { - onError_({ error }) - } - - try { - const thisConfig = settingsOfProvider.ollama - // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in - if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`) - - const ollama = new Ollama({ host: thisConfig.endpoint }) - ollama.list() - .then((response) => { - const { models } = response - onSuccess({ models }) - }) - .catch((error) => { - onError({ error: error + '' }) - }) - } - catch (error) { - onError({ error: error + '' }) - } -} - - - - -// export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - -// const thisConfig = settingsOfProvider.ollama -// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in -// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - -// let fullText = '' - -// const ollama = new Ollama({ host: thisConfig.endpoint }) - -// ollama.generate({ -// model: modelName, -// prompt: messages.prefix, -// suffix: messages.suffix, -// options: { -// stop: messages.stopTokens, -// num_predict: 300, // max tokens -// // repeat_penalty: 1, -// }, -// raw: true, -// stream: true, -// }) -// .then(async stream => { -// _setAborter(() => stream.abort()) -// // iterate through the stream -// for await (const chunk of stream) { -// const newText = chunk.response; -// fullText += newText; -// onText({ newText, fullText }); -// } -// onFinalMessage({ fullText, tools: [] }); -// }) -// // when error/fail -// .catch((error) => { -// onError({ message: error + '', fullError: error }) -// }) -// }; - - -// // Ollama -// export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - -// const thisConfig = settingsOfProvider.ollama -// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in -// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - -// let fullText = '' - -// const ollama = new Ollama({ host: thisConfig.endpoint }) - -// ollama.chat({ -// model: modelName, -// messages: messages, -// stream: true, -// // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens -// }) -// .then(async stream => { -// _setAborter(() => stream.abort()) -// // iterate through the stream -// for await (const chunk of stream) { -// const newText = chunk.message.content; - -// // chunk.message.tool_calls[0].function.arguments - -// fullText += newText; -// onText({ newText, fullText }); -// } - -// onFinalMessage({ fullText, tools: [] }); - -// }) -// // when error/fail -// .catch((error) => { -// onError({ message: error + '', fullError: error }) -// }) - -// }; - - - -// // ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',] diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts deleted file mode 100644 index 7769a983..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ /dev/null @@ -1,231 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import OpenAI from 'openai'; -import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; -import { Model } from 'openai/resources/models.js'; -import { InternalToolInfo } from '../../common/toolsService.js'; -import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; -import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; -import { isAToolName } from './postprocessToolCalls.js'; - - -// developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command -// prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting - -// npm i @openrouter/ai-sdk-provider ai ollama-ai-provider - -export const toOpenAITool = (toolInfo: InternalToolInfo) => { - const { name, description, params, required } = toolInfo - return { - type: 'function', - function: { - name: name, - description: description, - parameters: { - type: 'object', - properties: params, - required: required, - } - } - } satisfies OpenAI.Chat.Completions.ChatCompletionTool -} - - - - - -type NewParams = Pick[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'> -const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { - - if (providerName === 'openAI') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true - }) - } - else if (providerName === 'ollama') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'vLLM') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'openRouter') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - defaultHeaders: { - 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. - 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. - }, - }) - } - else if (providerName === 'gemini') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'deepseek') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'openAICompatible') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'mistral') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'groq') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'xAI') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else { - console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`) - throw new Error(`Void providerName was invalid: ${providerName}`) - } -} - - - -// might not currently be used in the code -export const openaiCompatibleList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { - const onSuccess = ({ models }: { models: Model[] }) => { - onSuccess_({ models }) - } - - const onError = ({ error }: { error: string }) => { - onError_({ error }) - } - - try { - const openai = newOpenAI({ providerName: 'openAICompatible', settingsOfProvider }) - - openai.models.list() - .then(async (response) => { - const models: Model[] = [] - models.push(...response.data) - while (response.hasNextPage()) { - models.push(...(await response.getNextPage()).data) - } - onSuccess({ models }) - }) - .catch((error) => { - onError({ error: error + '' }) - }) - } - catch (error) { - onError({ error: error + '' }) - } -} - - - - -export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => { - - - // openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models - - - -} - - - -// OpenAI, OpenRouter, OpenAICompatible -export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { - - let fullText = '' - const toolCallOfIndex: { [index: string]: { name: string, params: string, id: string } } = {} - - const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const { supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) - - const { messages } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: false }) - - const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAITool(tool)) : undefined - - const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider }) - const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { - model: modelName, - messages: messages, - stream: true, - tools: tools, - tool_choice: tools ? 'auto' : undefined, - parallel_tool_calls: tools ? false : undefined, - } - - openai.chat.completions - .create(options) - .then(async response => { - _setAborter(() => response.controller.abort()) - - // when receive text - for await (const chunk of response) { - - // tool call - for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) { - const index = tool.index - if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', params: '', id: '' } - toolCallOfIndex[index].name += tool.function?.name ?? '' - toolCallOfIndex[index].params += tool.function?.arguments ?? ''; - toolCallOfIndex[index].id = tool.id ?? '' - - } - - // message - let newText = '' - newText += chunk.choices[0]?.delta?.content ?? '' - console.log('!!!!', JSON.stringify(chunk, null, 2)) - fullText += newText; - - onText({ newText, fullText }); - } - onFinalMessage({ - fullText, - toolCalls: Object.keys(toolCallOfIndex) - .map(index => { - const tool = toolCallOfIndex[index] - if (isAToolName(tool.name)) - return { name: tool.name, id: tool.id, params: tool.params } - return null - }) - .filter(t => !!t) - }); - }) - // when error/fail - this catches errors of both .create() and .then(for await) - .catch(error => { - if (error instanceof OpenAI.APIError && error.status === 401) { - onError({ message: 'Invalid API key.', fullError: error }); - } - else { - onError({ message: error + '', fullError: error }); - } - }) - -} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts deleted file mode 100644 index aee52dcb..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ToolName, toolNames } from '../../common/toolsService.js'; - -const toolNamesSet = new Set(toolNames) - -export const isAToolName = (toolName: string): toolName is ToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName -} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 689e44de..3cef5327 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -1,7 +1,6 @@ import { LLMChatMessage } from '../../common/llmMessageTypes.js'; -import { developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js'; import { deepClone } from '../../../../../base/common/objects.js'; @@ -14,16 +13,24 @@ export const parseObject = (args: unknown) => { return {} } -// no matter whether the model supports a system message or not (or what format it supports), add it in some way -// also take into account tools if the model doesn't support tool use -export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], aiInstructions: string, { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: any[] } => { +const prepareMessages_cloneAndTrim = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => { const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), })) + return { messages } +} - const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const { supportsSystemMessage, supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) +// no matter whether the model supports a system message or not (or what format it supports), add it in some way +const prepareMessages_systemMessage = ({ + messages, + aiInstructions, + supportsSystemMessage, +}: { + messages: LLMChatMessage[], + aiInstructions: string, + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', +}) + : { separateSystemMessageStr?: string, messages: any[] } => { - // 1. SYSTEM MESSAGE // find system messages and concatenate them let systemMessageStr = messages .filter(msg => msg.role === 'system') @@ -33,7 +40,6 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: if (aiInstructions) systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}` - let separateSystemMessageStr: string | undefined = undefined // remove all system messages @@ -49,11 +55,12 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: if (systemMessageStr) { // if supports system message if (supportsSystemMessage) { - if (separateSystemMessage) + if (supportsSystemMessage === 'separated') separateSystemMessageStr = systemMessageStr - else { - newMessages.unshift({ role: supportsSystemMessage === 'developer' ? 'developer' : 'system', content: systemMessageStr }) // add new first message - } + else if (supportsSystemMessage === 'system-role') + newMessages.unshift({ role: 'system', content: systemMessageStr }) // add new first message + else if (supportsSystemMessage === 'developer-role') + newMessages.unshift({ role: 'developer', content: systemMessageStr }) // add new first message } // if does not support system message else { @@ -79,181 +86,179 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: } } + return { messages: newMessages, separateSystemMessageStr } +} - // 2. MAKE TOOLS FORMAT CORRECT in messages - let finalMessages: any[] - if (!supportsTools) { - // do nothing - finalMessages = newMessages - } +// convert messages as if about to send to openai +/* +reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps +openai MESSAGE (role=assistant): +"tool_calls":[{ + "type": "function", + "id": "call_12345xyz", + "function": { + "name": "get_weather", + "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" +}] - // anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples - // "content": [ - // { - // "type": "text", - // "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." - // }, - // { - // "type": "tool_use", - // "id": "toolu_01A09q90qw90lq917835lq9", - // "name": "get_weather", - // "input": { "location": "San Francisco, CA", "unit": "celsius" } - // } - // ] +openai RESPONSE (role=user): +{ "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result) } - // anthropic user message response will be: - // "content": [ - // { - // "type": "tool_result", - // "tool_use_id": "toolu_01A09q90qw90lq917835lq9", - // "content": "15 degrees" - // } - // ] +also see +openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting +openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command +*/ +const prepareMessages_tools_openai = ({ messages }: { messages: LLMChatMessage[], }) => { - else if (providerName === 'anthropic') { // convert role:'tool' to anthropic's type - const newMessagesTools: ( - Exclude | { - role: 'assistant', - content: string | ({ - type: 'text'; - text: string; - } | { - type: 'tool_use'; + const newMessages: ( + Exclude | { + role: 'assistant', + content: string; + tool_calls?: { + type: 'function'; + id: string; + function: { name: string; - input: Record; - id: string; - })[] - } | { - role: 'user', - content: string | ({ - type: 'text'; - text: string; - } | { - type: 'tool_result'; - tool_use_id: string; - content: string; - })[] - } - )[] = newMessages; + arguments: string; + } + }[] + } | { + role: 'tool', + id: string; // old val + tool_call_id: string; // new val + content: string; + } + )[] = []; + for (let i = 0; i < messages.length; i += 1) { + const currMsg = messages[i] - for (let i = 0; i < newMessagesTools.length; i += 1) { - const currMsg = newMessagesTools[i] - - if (currMsg.role !== 'tool') continue - - const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined - - if (prevMsg?.role === 'assistant') { - if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }] - prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) }) - } - - // turn each tool into a user message with tool results at the end - newMessagesTools[i] = { - role: 'user', - content: [ - ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const, - ...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [], - ] - } + if (currMsg.role !== 'tool') { + newMessages.push(currMsg) + continue } - finalMessages = newMessagesTools - } - - // openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps - // "tool_calls":[ - // { - // "type": "function", - // "id": "call_12345xyz", - // "function": { - // "name": "get_weather", - // "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" - // } - // }] - - // openai user response will be: - // { - // "role": "tool", - // "tool_call_id": tool_call.id, - // "content": str(result) - // } - - // treat all other providers like openai tool message for now - else { - - const newMessagesTools: ( - Exclude | { - role: 'assistant', - content: string; - tool_calls?: { - type: 'function'; - id: string; - function: { - name: string; - arguments: string; - } - }[] - } | { - role: 'tool', - id: string; // old val - tool_call_id: string; // new val - content: string; - } - )[] = []; - - for (let i = 0; i < newMessages.length; i += 1) { - const currMsg = newMessages[i] - - if (currMsg.role !== 'tool') { - newMessagesTools.push(currMsg) - continue - } - - // edit previous assistant message to have called the tool - const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined - if (prevMsg?.role === 'assistant') { - prevMsg.tool_calls = [{ - type: 'function', - id: currMsg.id, - function: { - name: currMsg.name, - arguments: JSON.stringify(currMsg.params) - } - }] - } - - // add the tool - newMessagesTools.push({ - role: 'tool', + // edit previous assistant message to have called the tool + const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined + if (prevMsg?.role === 'assistant') { + prevMsg.tool_calls = [{ + type: 'function', id: currMsg.id, - content: currMsg.content, - tool_call_id: currMsg.id, - }) + function: { + name: currMsg.name, + arguments: JSON.stringify(currMsg.params) + } + }] } - finalMessages = newMessagesTools + + // add the tool + newMessages.push({ + role: 'tool', + id: currMsg.id, + content: currMsg.content, + tool_call_id: currMsg.id, + }) } + return { messages: newMessages } + +} + + +// convert messages as if about to send to anthropic +/* +https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples +anthropic MESSAGE (role=assistant): +"content": [{ + "type": "text", + "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." +}, { + "type": "tool_use", + "id": "toolu_01A09q90qw90lq917835lq9", + "name": "get_weather", + "input": { "location": "San Francisco, CA", "unit": "celsius" } +}] +anthropic RESPONSE (role=user): +"content": [{ + "type": "tool_result", + "tool_use_id": "toolu_01A09q90qw90lq917835lq9", + "content": "15 degrees" +}] +*/ + +const prepareMessages_tools_anthropic = ({ messages }: { messages: LLMChatMessage[], }) => { + const newMessages: ( + Exclude | { + role: 'assistant', + content: string | ({ + type: 'text'; + text: string; + } | { + type: 'tool_use'; + name: string; + input: Record; + id: string; + })[] + } | { + role: 'user', + content: string | ({ + type: 'text'; + text: string; + } | { + type: 'tool_result'; + tool_use_id: string; + content: string; + })[] + } + )[] = messages; + + + for (let i = 0; i < newMessages.length; i += 1) { + const currMsg = newMessages[i] + + if (currMsg.role !== 'tool') continue + + const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined + + if (prevMsg?.role === 'assistant') { + if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }] + prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) }) + } + + // turn each tool into a user message with tool results at the end + newMessages[i] = { + role: 'user', + content: [ + ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const, + ...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [], + ] + } + } + return { messages: newMessages } +} - // 3. CROP MESSAGES SO EVERYTHING FITS IN CONTEXT - // TODO!!! - - console.log('SYSMG', separateSystemMessage) - console.log('FINAL MESSAGES', JSON.stringify(finalMessages, null, 2)) - - - return { - separateSystemMessageStr, - messages: finalMessages, +const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => { + if (!supportsTools) { + return { messages: messages } + } + else if (supportsTools === 'anthropic-style') { + return prepareMessages_tools_anthropic({ messages }) + } + else if (supportsTools === 'openai-style') { + return prepareMessages_tools_openai({ messages }) + } + else { + throw 1 } } @@ -262,42 +267,59 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: + + + + + + + + + + +export const prepareMessages = ({ + messages, + aiInstructions, + supportsSystemMessage, + supportsTools, +}: { + messages: LLMChatMessage[], + aiInstructions: string, + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', + supportsTools: false | 'anthropic-style' | 'openai-style', +}) => { + const { messages: messages1 } = prepareMessages_cloneAndTrim({ messages }) + const { messages: messages2, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages1, aiInstructions, supportsSystemMessage }) + const { messages: messages3 } = prepareMessages_tools({ messages: messages2, supportsTools }) + return { + messages: messages3 as any, + separateSystemMessageStr + } as const +} + + /* - - ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) -gemini request: { -"role": "assistant", -"content": null, -"function_call": { -"name": "get_weather", -"arguments": { -"latitude": 48.8566, -"longitude": 2.3522 -} -} +gemini request: +{ "role": "assistant", + "content": null, + "function_call": { + "name": "get_weather", + "arguments": { + "latitude": 48.8566, + "longitude": 2.3522 + } + } } + gemini response: -{ -"role": "assistant", -"function_response": { -"name": "get_weather", -"response": { -"temperature": "15°C", -"condition": "Cloudy" +{ "role": "assistant", + "function_response": { + "name": "get_weather", + "response": { + "temperature": "15°C", + "condition": "Cloudy" + } + } } -} -} - - -+ anthropic - -+ openai-compat (4) -+ gemini - -ollama - - -mistral: same as openai - */ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 7bb4e141..1c9ac21d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -6,9 +6,7 @@ import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; - -import { sendAnthropicChat } from './anthropic.js'; -import { sendOpenAIChat } from './openai.js'; +import { sendAnthropicChat, sendOpenAIChat } from './MODELS.js'; export const sendLLMMessage = ({ @@ -97,6 +95,10 @@ export const sendLLMMessage = ({ try { switch (providerName) { + case 'anthropic': + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' }) + else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); + break; case 'openAI': case 'openRouter': case 'deepseek': @@ -107,13 +109,9 @@ export const sendLLMMessage = ({ case 'groq': case 'gemini': case 'xAI': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI API FIM', toolCalls: [] }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI API FIM' }) else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); break; - case 'anthropic': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', toolCalls: [] }) - else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); - break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) break; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts index b00ade9c..929c85e4 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts @@ -11,8 +11,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js'; import { sendLLMMessage } from './llmMessage/sendLLMMessage.js' import { IMetricsService } from '../common/metricsService.js'; -import { ollamaList } from './llmMessage/ollama.js'; -import { openaiCompatibleList } from './llmMessage/openai.js'; +import { ollamaList } from './llmMessage/MODELS.js'; // NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it From fd5e5234348e6ccb2db44234d5ecd398ef74f4e4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 23 Feb 2025 16:58:43 -0800 Subject: [PATCH 86/92] models --- .vscode/settings.json | 2 +- .../contrib/void/common/llmMessageTypes.ts | 41 +- .../contrib/void/common/toolsService.ts | 7 + .../void/electron-main/llmMessage/MODELS.ts | 113 ++-- .../void/electron-main/llmMessage/_old.ts | 483 ------------------ 5 files changed, 73 insertions(+), 573 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index d24ab6e5..910e0bec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -113,7 +113,7 @@ "files.insertFinalNewline": false }, "[typescript]": { - "editor.defaultFormatter": "ms-vsliveshare.vsliveshare", + "editor.defaultFormatter": "vscode.typescript-language-features", "editor.formatOnSave": true }, "[javascript]": { diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 58989cce..01e03ad4 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -65,7 +65,7 @@ export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { } -type _InternalSendFIMMessage = { +export type LLMFIMMessage = { prefix: string; suffix: string; stopTokens: string[]; @@ -77,7 +77,7 @@ type SendLLMType = { tools?: InternalToolInfo[]; } | { messagesType: 'FIMMessage'; - messages: _InternalSendFIMMessage; + messages: LLMFIMMessage; tools?: undefined; } @@ -118,38 +118,6 @@ export type EventLLMMessageOnFinalMessageParams = Parameters[0] export type EventLLMMessageOnErrorParams = Parameters[0] & { requestId: string } -export type _InternalSendLLMChatMessageFnType = ( - params: { - aiInstructions: string; - - onText: OnText; - onFinalMessage: OnFinalMessage; - onError: OnError; - providerName: ProviderName; - settingsOfProvider: SettingsOfProvider; - modelName: string; - _setAborter: (aborter: () => void) => void; - - tools?: InternalToolInfo[], - - messages: LLMChatMessage[]; - } -) => void - -export type _InternalSendLLMFIMMessageFnType = ( - params: { - onText: OnText; - onFinalMessage: OnFinalMessage; - onError: OnError; - providerName: ProviderName; - settingsOfProvider: SettingsOfProvider; - modelName: string; - _setAborter: (aborter: () => void) => void; - - messages: _InternalSendFIMMessage; - } -) => void - // service -> main -> internal -> event (back to main) // (browser) @@ -190,10 +158,10 @@ export type OpenaiCompatibleModelResponse = { // params to the true list fn -export type ModelListParams = { +export type ModelListParams = { providerName: ProviderName; settingsOfProvider: SettingsOfProvider; - onSuccess: (param: { models: modelResponse[] }) => void; + onSuccess: (param: { models: ModelResponse[] }) => void; onError: (param: { error: string }) => void; } @@ -212,4 +180,3 @@ export type EventModelListOnErrorParams = Parameters = (params: ModelListParams) => void diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 09ce82a2..4e92696c 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -89,6 +89,13 @@ export const voidTools = { export type ToolName = keyof typeof voidTools export const toolNames = Object.keys(voidTools) as ToolName[] +const toolNamesSet = new Set(toolNames) +export const isAToolName = (toolName: string): toolName is ToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + export type ToolParamNames = keyof typeof voidTools[T]['params'] export type ToolParamsObj = { [paramName in ToolParamNames]: unknown } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index 3cefdfa9..e6e035fe 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -5,8 +5,8 @@ import OpenAI from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; -import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; -import { InternalToolInfo, ToolName, toolNames } from '../../common/toolsService.js'; +import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; +import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; import { defaultProviderSettings, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareMessages } from './preprocessLLMMessages.js'; import Anthropic from '@anthropic-ai/sdk'; @@ -80,12 +80,13 @@ type ProviderSettings = { output: number; cache_read?: number; cache_write?: number; - }; + } supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; supportsTools: false | 'anthropic-style' | 'openai-style'; - }; - }; -}; + supportsFIM: false | 'TODO_FIM_FORMAT' + } + } +} const openAIProviderSettings: ProviderSettings = { @@ -97,21 +98,24 @@ const openAIProviderSettings: ProviderSettings = { FIMFormat: '', modelOptions: { - "o1": { + 'o1': { contextWindow: 128_000, cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, + supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', }, - "o3-mini": { + 'o3-mini': { contextWindow: 200_000, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', }, - "gpt-4o": { + 'gpt-4o': { contextWindow: 128_000, cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, + supportsFIM: false, supportsTools: 'openai-style', supportsSystemMessage: 'system-role', }, @@ -134,6 +138,7 @@ const anthropicProviderSettings: ProviderSettings = { "claude-3-5-sonnet-20241022": { contextWindow: 200_000, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'anthropic-style', @@ -141,17 +146,20 @@ const anthropicProviderSettings: ProviderSettings = { "claude-3-5-haiku-20241022": { contextWindow: 200_000, cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'anthropic-style', }, "claude-3-opus-20240229": { contextWindow: 200_000, cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'anthropic-style', }, "claude-3-sonnet-20240229": { contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'anthropic-style', } @@ -168,30 +176,13 @@ const grokProviderSettings: ProviderSettings = { FIMFormat: '', modelOptions: { - "claude-3-5-sonnet-20241022": { - contextWindow: 200_000, - cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + "grok-2-latest": { + contextWindow: 131_072, + cost: { input: 2.00, output: 10.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - + supportsTools: 'openai-style', }, - "claude-3-5-haiku-20241022": { - contextWindow: 200_000, - cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - }, - "claude-3-opus-20240229": { - contextWindow: 200_000, - cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - }, - "claude-3-sonnet-20240229": { - contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - } } } @@ -199,14 +190,22 @@ const grokProviderSettings: ProviderSettings = { -// helpers -const toolNamesSet = new Set(toolNames) -const isAToolName = (toolName: string): toolName is ToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName +type InternalCommonMessageParams = { + aiInstructions: string; + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: OnError; + providerName: ProviderName; + settingsOfProvider: SettingsOfProvider; + modelName: string; + _setAborter: (aborter: () => void) => void; } +type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; tools?: InternalToolInfo[] } +type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; } +export type ListParams_Internal = ModelListParams + // ------------ OPENAI-COMPATIBLE (HELPERS) ------------ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { @@ -251,7 +250,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName }: { settings else throw new Error(`Invalid providerName ${providerName}`) } -export const _sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { +export const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: '', supportsTools: '', }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined @@ -291,7 +290,7 @@ export const _sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = ({ m } -export const _openaiCompatibleList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }) => { +export const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OpenAIModel[] }) => { onSuccess_({ models }) } @@ -320,7 +319,7 @@ export const _openaiCompatibleList: _InternalModelListFnType = asyn // ------------ OPENAI ------------ -export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = (params) => { +export const sendOpenAIChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) } @@ -346,7 +345,7 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[ }).filter(t => !!t) } -export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => { +export const sendAnthropicChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', }) const thisConfig = settingsOfProvider.anthropic @@ -405,7 +404,7 @@ const newOllamaSDK = ({ endpoint }: { endpoint: string }) => { return ollama } -export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { +export const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => { onSuccess_({ models }) } @@ -429,7 +428,7 @@ export const ollamaList: _InternalModelListFnType = async ( } } -export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { const thisConfig = settingsOfProvider.ollama const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) @@ -462,7 +461,7 @@ export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFi // ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat! -export const sendOllamaMessage: _InternalSendLLMChatMessageFnType = (params) => { +export const sendOllamaChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) // TODO!!! filter out reasoning tags... } @@ -470,24 +469,24 @@ export const sendOllamaMessage: _InternalSendLLMChatMessageFnType = (params) => // ------------ OPENROUTER ------------ -export const sendOpenRouterFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendOpenRouterFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { // TODO!!! } -export const sendOpenRouterChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { - // payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models - // response.choices[0].delta.reasoning +export const sendOpenRouterChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + // reasoning: response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models + // } // ------------ OPENAI-COMPATIBLE ------------ -export const openAICompatibleList: _InternalModelListFnType = async (params) => { +export const openAICompatibleList = async (params: ListParams_Internal) => { return _openaiCompatibleList(params) } // TODO!!! FIM // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -export const sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = (params) => { +export const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) } @@ -496,13 +495,23 @@ export const sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = (para // TODO!!! FIM // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -export const sendVLLMChat: _InternalSendLLMChatMessageFnType = (params) => { +export const sendVLLMChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) - // response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions + // reasoning: response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions } - +// ------------ DEEPSEEK API ------------ +export const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) + // reasoning: response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model +} + + +// ------------ GEMINI ------------ +// ------------ MISTRAL ------------ +// ------------ GROQ ------------ +// ------------ GROK ------------ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts deleted file mode 100644 index 8aa80bf4..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts +++ /dev/null @@ -1,483 +0,0 @@ -// /*-------------------------------------------------------------------------------------- -// * Copyright 2025 Glass Devtools, Inc. All rights reserved. -// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. -// *--------------------------------------------------------------------------------------*/ - -// import Groq from 'groq-sdk'; -// import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// // Groq -// export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { -// let fullText = ''; - -// const thisConfig = settingsOfProvider.groq - -// const groq = new Groq({ -// apiKey: thisConfig.apiKey, -// dangerouslyAllowBrowser: true -// }); - -// await groq.chat.completions -// .create({ -// messages: messages, -// model: modelName, -// stream: true, -// }) -// .then(async response => { -// _setAborter(() => response.controller.abort()) -// // when receive text -// for await (const chunk of response) { -// const newText = chunk.choices[0]?.delta?.content || ''; -// fullText += newText; -// onText({ newText, fullText }); -// } - -// onFinalMessage({ fullText, tools: [] }); -// }) -// .catch(error => { -// onError({ message: error + '', fullError: error }); -// }) - - -// }; - - - -// /*-------------------------------------------------------------------------------------- -// * Copyright 2025 Glass Devtools, Inc. All rights reserved. -// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. -// *--------------------------------------------------------------------------------------*/ - -// import { Mistral } from '@mistralai/mistralai'; -// import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// // Mistral -// export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { -// let fullText = ''; - -// const thisConfig = settingsOfProvider.mistral; - -// const mistral = new Mistral({ -// apiKey: thisConfig.apiKey, -// }) - -// await mistral.chat -// .stream({ -// messages: messages, -// model: modelName, -// stream: true, -// }) -// .then(async response => { -// // Mistral has a really nonstandard API - no interrupt and weird stream types -// _setAborter(() => { console.log('Mistral does not support interrupts! Further messages will just be ignored.') }); -// // when receive text -// for await (const chunk of response) { -// const c = chunk.data.choices[0].delta.content || '' -// const newText = ( -// typeof c === 'string' ? c -// : c?.map(c => c.type === 'text' ? c.text : c.type).join('\n') -// ) -// fullText += newText; -// onText({ newText, fullText }); -// } - -// onFinalMessage({ fullText, tools: [] }); -// }) -// .catch(error => { -// onError({ message: error + '', fullError: error }); -// }) -// } - - - - - - - - - - - - - - - - - - - - - - - - - - -// export const recognizedModels = [ -// // chat -// 'OpenAI 4o', -// 'Anthropic Claude', -// 'Llama 3.x', -// 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model -// 'xAI Grok', -// // 'xAI Grok', -// // 'Google Gemini, Gemma', -// // 'Microsoft Phi4', - - -// // coding (autocomplete) -// 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 -// 'Mistral Codestral', - -// // thinking -// 'OpenAI o1', -// 'Deepseek R1', - -// // general -// // 'Mixtral 8x7b' -// // 'Qwen2.5', - -// ] as const - -// type RecognizedModelName = (typeof recognizedModels)[number] | '' - - -// export function recognizedModelOfModelName(modelName: string): RecognizedModelName { -// const lower = modelName.toLowerCase(); - -// if (lower.includes('gpt-4o')) -// return 'OpenAI 4o'; -// if (lower.includes('claude')) -// return 'Anthropic Claude'; -// if (lower.includes('llama')) -// return 'Llama 3.x'; -// if (lower.includes('qwen2.5-coder')) -// return 'Alibaba Qwen2.5 Coder Instruct'; -// if (lower.includes('mistral')) -// return 'Mistral Codestral'; -// if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 -// return 'OpenAI o1'; -// if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) -// return 'Deepseek R1'; -// if (lower.includes('deepseek')) -// return 'Deepseek Chat' -// if (lower.includes('grok')) -// return 'xAI Grok' - -// return ''; -// } - - -// const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { -// 'anthropic': { -// overrideSettingsForAllModels: { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: true, -// } -// }, -// 'deepseek': { -// overrideSettingsForAllModels: { -// } -// }, -// 'ollama': { -// }, -// 'openRouter': { -// }, -// 'openAICompatible': { -// }, -// 'openAI': { -// }, -// 'gemini': { -// }, -// 'mistral': { -// }, -// 'groq': { -// }, -// 'xAI': { -// }, -// 'vLLM': { -// }, -// } -// export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { -// return developerInfoAtProvider[providerName] ?? {} -// } - - - - -// // providerName is optional, but gives some extra fallbacks if provided -// const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { -// 'OpenAI 4o': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: true, -// _maxTokens: 4096, -// }, - -// 'Anthropic Claude': { -// supportsSystemMessage: true, -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'Llama 3.x': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'xAI Grok': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: true, -// _maxTokens: 4096, - -// }, - -// 'Deepseek Chat': { -// supportsSystemMessage: true, -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'Alibaba Qwen2.5 Coder Instruct': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'Mistral Codestral': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'OpenAI o1': { -// supportsSystemMessage: 'developer', -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: true, -// _maxTokens: 4096, -// }, - -// 'Deepseek R1': { -// supportsSystemMessage: false, -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - - -// '': { -// supportsSystemMessage: false, -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, -// } -// export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { -// const recognizedModelName = recognizedModelOfModelName(modelName) -// return { -// _recognizedModelName: recognizedModelName, -// ...developerInfoOfRecognizedModelName[recognizedModelName], -// ...overrides -// } -// } - - - - - - -// // creates `modelInfo` from `modelNames` - - - - - -// export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { -// const { existingModels } = options - -// const existingModelsMap: Record = {} -// for (const existingModel of existingModels) { -// existingModelsMap[existingModel.modelName] = existingModel -// } - -// return defaultModelNames.map((modelName, i) => ({ -// modelName, -// isDefault: true, -// isAutodetected: true, -// isHidden: !!existingModelsMap[modelName]?.isHidden, -// ...developerInfoOfModelName(modelName) -// })) -// } - - - - - - -// export const anthropicMaxPossibleTokens = (modelName: string) => { -// if (modelName === 'claude-3-5-sonnet-20241022' -// || modelName === 'claude-3-5-haiku-20241022') -// return 8192 -// if (modelName === 'claude-3-opus-20240229' -// || modelName === 'claude-3-sonnet-20240229' -// || modelName === 'claude-3-haiku-20240307') -// return 4096 -// return 1024 // return a reasonably small number if they're using a different model -// } - - - - - - - - - - - - - - - - - - - -// // Ollama chat -// export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - -// const thisConfig = settingsOfProvider.ollama -// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in -// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - -// let fullText = '' - -// const ollama = new Ollama({ host: thisConfig.endpoint }) - -// ollama.chat({ -// model: modelName, -// messages: messages, -// stream: true, -// // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens -// }) -// .then(async stream => { -// _setAborter(() => stream.abort()) -// // iterate through the stream -// for await (const chunk of stream) { -// const newText = chunk.message.content; - -// // chunk.message.tool_calls[0].function.arguments - -// fullText += newText; -// onText({ newText, fullText }); -// } - -// onFinalMessage({ fullText, tools: [] }); - -// }) -// // when error/fail -// .catch((error) => { -// onError({ message: error + '', fullError: error }) -// }) - -// }; - - - - - - - -// type NewParams = Pick[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'> -// const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { - -// if (providerName === 'openAI') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true -// }) -// } -// else if (providerName === 'ollama') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'vLLM') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'openRouter') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// defaultHeaders: { -// 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. -// 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. -// }, -// }) -// } -// else if (providerName === 'gemini') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'deepseek') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'openAICompatible') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'mistral') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'groq') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'xAI') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else { -// console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`) -// throw new Error(`Void providerName was invalid: ${providerName}`) -// } -// } - - From 9f20476eea682cdab5e9757b7844604b8eb5252b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 23 Feb 2025 21:37:34 -0800 Subject: [PATCH 87/92] provider support progress Co-authored-by: Mathew Pareles --- .../browser/helpers/extractCodeFromResult.ts | 21 +- .../contrib/void/common/llmMessageService.ts | 109 ++-- .../contrib/void/common/llmMessageTypes.ts | 7 +- .../void/common/refreshModelService.ts | 10 +- .../void/electron-main/llmMessage/MODELS.ts | 588 +++++++++++++----- .../llmMessage/preprocessLLMMessages.ts | 51 +- .../llmMessage/sendLLMMessage.ts | 47 +- .../void/electron-main/llmMessageChannel.ts | 133 ++-- 8 files changed, 602 insertions(+), 364 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index cd3276ff..806676da 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -59,7 +59,7 @@ class SurroundingsRemover { // return offset === suffix.length // } - removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => { + removeFromStartUntilFullMatch = (until: string, alsoRemoveUntilStr: boolean) => { const index = this.originalS.indexOf(until, this.i) if (index === -1) { @@ -86,7 +86,7 @@ class SurroundingsRemover { const foundCodeBlock = pm.removePrefix('```') if (!foundCodeBlock) return false - pm.removeFromStartUntil('\n', true) // language + pm.removeFromStartUntilFullMatch('\n', true) // language const j = pm.j let foundCodeBlockEnd = pm.removeSuffix('```') @@ -159,27 +159,10 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen) return [s, delta, ignoredSuffix] - - - // // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?([\s\S]*?)(?:<\/MID>|`{1,3}|$)/; - // const regex = new RegExp( - // `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:|\`{1,3}|$)`, - // '' - // ); - // const match = text.match(regex); - // if (match) { - // const [_, languageName, codeBetweenMidTags] = match; - // return [languageName, codeBetweenMidTags] as const - - // } else { - // return [undefined, extractCodeFromRegular(text)] as const - // } - } - export type ExtractedSearchReplaceBlock = { state: 'writingOriginal' | 'writingFinal' | 'done', orig: string, diff --git a/src/vs/workbench/contrib/void/common/llmMessageService.ts b/src/vs/workbench/contrib/void/common/llmMessageService.ts index c8f9ea2c..b2266213 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageService.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageService.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './llmMessageTypes.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; @@ -24,27 +24,39 @@ export interface ILLMMessageService { sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null; abort: (requestId: string) => void; ollamaList: (params: ServiceModelListParams) => void; - openAICompatibleList: (params: ServiceModelListParams) => void; + vLLMList: (params: ServiceModelListParams) => void; } + +// open this file side by side with llmMessageChannel export class LLMMessageService extends Disposable implements ILLMMessageService { readonly _serviceBrand: undefined; private readonly channel: IChannel // LLMMessageChannel - // llmMessage - private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {} - private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {} - private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {} + // sendLLMMessage + private readonly llmMessageHooks = { + onText: {} as { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) }, + onFinalMessage: {} as { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) }, + onError: {} as { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) }, + } - - // ollamaList - private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) } = {} - private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams) => void) } = {} - - // openAICompatibleList - private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) } = {} - private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams) => void) } = {} + // list hooks + private readonly listHooks = { + ollama: { + success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) }, + error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams) => void) }, + }, + vLLM: { + success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) }, + error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams) => void) }, + } + } satisfies { + [providerName: string]: { + success: { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) }, + error: { [eventId: string]: ((params: EventModelListOnErrorParams) => void) }, + } + } constructor( @IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side) @@ -59,32 +71,14 @@ export class LLMMessageService extends Disposable implements ILLMMessageService // .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead // llm - this._register((this.channel.listen('onText_llm') satisfies Event)(e => { - this.onTextHooks_llm[e.requestId]?.(e) - })) - this._register((this.channel.listen('onFinalMessage_llm') satisfies Event)(e => { - this.onFinalMessageHooks_llm[e.requestId]?.(e) - this._onRequestIdDone(e.requestId) - })) - this._register((this.channel.listen('onError_llm') satisfies Event)(e => { - console.error('Error in LLMMessageService:', JSON.stringify(e)) - this.onErrorHooks_llm[e.requestId]?.(e) - this._onRequestIdDone(e.requestId) - })) + this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) })) + this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._onRequestIdDone(e.requestId) })) + this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._onRequestIdDone(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) })) // ollama .list() - this._register((this.channel.listen('onSuccess_ollama') satisfies Event>)(e => { - this.onSuccess_ollama[e.requestId]?.(e) - })) - this._register((this.channel.listen('onError_ollama') satisfies Event>)(e => { - this.onError_ollama[e.requestId]?.(e) - })) - // openaiCompatible .list() - this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event>)(e => { - this.onSuccess_openAICompatible[e.requestId]?.(e) - })) - this._register((this.channel.listen('onError_openAICompatible') satisfies Event>)(e => { - this.onError_openAICompatible[e.requestId]?.(e) - })) + this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event>)(e => { this.listHooks.ollama.success[e.requestId]?.(e) })) + this._register((this.channel.listen('onError_list_ollama') satisfies Event>)(e => { this.listHooks.ollama.error[e.requestId]?.(e) })) + this._register((this.channel.listen('onSuccess_list_vLLM') satisfies Event>)(e => { this.listHooks.vLLM.success[e.requestId]?.(e) })) + this._register((this.channel.listen('onError_list_vLLM') satisfies Event>)(e => { this.listHooks.vLLM.error[e.requestId]?.(e) })) } @@ -117,9 +111,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService // add state for request id const requestId = generateUuid(); - this.onTextHooks_llm[requestId] = onText - this.onFinalMessageHooks_llm[requestId] = onFinalMessage - this.onErrorHooks_llm[requestId] = onError + this.llmMessageHooks.onText[requestId] = onText + this.llmMessageHooks.onFinalMessage[requestId] = onFinalMessage + this.llmMessageHooks.onError[requestId] = onError const { aiInstructions } = this.voidSettingsService.state.globalSettings const { settingsOfProvider } = this.voidSettingsService.state @@ -151,8 +145,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService // add state for request id const requestId_ = generateUuid(); - this.onSuccess_ollama[requestId_] = onSuccess - this.onError_ollama[requestId_] = onError + this.listHooks.ollama.success[requestId_] = onSuccess + this.listHooks.ollama.error[requestId_] = onError this.channel.call('ollamaList', { ...proxyParams, @@ -163,33 +157,34 @@ export class LLMMessageService extends Disposable implements ILLMMessageService } - openAICompatibleList = (params: ServiceModelListParams) => { + vLLMList = (params: ServiceModelListParams) => { const { onSuccess, onError, ...proxyParams } = params const { settingsOfProvider } = this.voidSettingsService.state // add state for request id const requestId_ = generateUuid(); - this.onSuccess_openAICompatible[requestId_] = onSuccess - this.onError_openAICompatible[requestId_] = onError + this.listHooks.vLLM.success[requestId_] = onSuccess + this.listHooks.vLLM.error[requestId_] = onError - this.channel.call('openAICompatibleList', { + this.channel.call('vLLMList', { ...proxyParams, settingsOfProvider, - providerName: 'openAICompatible', + providerName: 'vLLM', requestId: requestId_, - } satisfies MainModelListParams) + } satisfies MainModelListParams) } - - _onRequestIdDone(requestId: string) { - delete this.onTextHooks_llm[requestId] - delete this.onFinalMessageHooks_llm[requestId] - delete this.onErrorHooks_llm[requestId] + delete this.llmMessageHooks.onText[requestId] + delete this.llmMessageHooks.onFinalMessage[requestId] + delete this.llmMessageHooks.onError[requestId] - delete this.onSuccess_ollama[requestId] - delete this.onError_ollama[requestId] + delete this.listHooks.ollama.success[requestId] + delete this.listHooks.ollama.error[requestId] + + delete this.listHooks.vLLM.success[requestId] + delete this.listHooks.vLLM.error[requestId] } } diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 01e03ad4..abe88970 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -45,7 +45,7 @@ export type ToolCallType = { } -export type OnText = (p: { newText: string, fullText: string }) => void +export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } @@ -149,13 +149,16 @@ export type OllamaModelResponse = { size_vram: number; } -export type OpenaiCompatibleModelResponse = { +type OpenaiCompatibleModelResponse = { id: string; created: number; object: 'model'; owned_by: string; } +export type VLLMModelResponse = OpenaiCompatibleModelResponse + + // params to the true list fn export type ModelListParams = { diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index 1c95a4ad..1d68b304 100644 --- a/src/vs/workbench/contrib/void/common/refreshModelService.ts +++ b/src/vs/workbench/contrib/void/common/refreshModelService.ts @@ -8,7 +8,7 @@ import { ILLMMessageService } from './llmMessageService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js'; -import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js'; +import { OllamaModelResponse, VLLMModelResponse } from './llmMessageTypes.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -160,9 +160,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ } } const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList - : providerName === 'vLLM' ? this.llmMessageService.openAICompatibleList - : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList - : () => { } + : providerName === 'vLLM' ? this.llmMessageService.vLLMList + : () => { } listFn({ onSuccess: ({ models }) => { @@ -172,8 +171,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ providerName, models.map(model => { if (providerName === 'ollama') return (model as OllamaModelResponse).name; - else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id; - else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id; + else if (providerName === 'vLLM') return (model as VLLMModelResponse).id; else throw new Error('refreshMode fn: unknown provider', providerName); }), { enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index e6e035fe..ce0d0537 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -3,11 +3,11 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import OpenAI from 'openai'; +import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; -import { defaultProviderSettings, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareMessages } from './preprocessLLMMessages.js'; import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; @@ -68,129 +68,215 @@ export const defaultModelsOfProvider = { +type ModelOptions = { + contextWindow: number; + cost: { + input: number; + output: number; + cache_read?: number; + cache_write?: number; + } + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; + supportsTools: false | 'anthropic-style' | 'openai-style'; + supportsFIM: false | 'TODO_FIM_FORMAT'; + + supportsReasoning: boolean; // not whether it reasons, but whether it outputs reasoning tokens + manualMatchReasoningTokens?: [string, string]; // reasoning tokens if it's an OSS model +} + +type ProviderReasoningOptions = { + // include this in payload to get reasoning + input?: { includeInPayload?: { [key: string]: any }, }; + // nameOfFieldInDelta: reasoning output is in response.choices[0].delta[deltaReasoningField] + // needsManualParse: whether we must manually parse out the tags + output?: + | { nameOfFieldInDelta?: string, needsManualParse?: undefined, } + | { nameOfFieldInDelta?: undefined, needsManualParse?: true, }; +} + type ProviderSettings = { - thinkingFormat: string; - toolsFormat: string; - FIMFormat: string; - modelOptions: { - [key: string]: { - contextWindow: number; - cost: { - input: number; - output: number; - cache_read?: number; - cache_write?: number; - } - supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; - supportsTools: false | 'anthropic-style' | 'openai-style'; - supportsFIM: false | 'TODO_FIM_FORMAT' + providerReasoningOptions?: ProviderReasoningOptions; + modelOptions: { [key: string]: ModelOptions }; + modelOptionsFallback: (modelName: string) => ModelOptions; // allowed to throw error if modeName is totally invalid +} + + +type ModelSettingsOfProvider = { + [providerName in ProviderName]: ProviderSettings +} + + + + + +const modelNotRecognizedErrorMessage = (modelName: string, providerName: ProviderName) => `Void could not find a model matching ${modelName} for ${displayInfoOfProviderName(providerName).title}.` + + + +// ---------------- OPENAI ---------------- +const openAIModelOptions = { + "o1": { + contextWindow: 128_000, + cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, + supportsFIM: false, + supportsTools: false, + supportsSystemMessage: 'developer-role', + supportsReasoning: false, + }, + "o3-mini": { + contextWindow: 200_000, + cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + supportsFIM: false, + supportsTools: false, + supportsSystemMessage: 'developer-role', + supportsReasoning: false, + }, + "gpt-4o": { + contextWindow: 128_000, + cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, + supportsFIM: false, + supportsTools: 'openai-style', + supportsSystemMessage: 'system-role', + supportsReasoning: false, + }, +} as const + +const openAISettings: ProviderSettings = { + modelOptions: openAIModelOptions, + modelOptionsFallback: (modelName) => { + if (modelName.includes('o1')) return openAIModelOptions['o1'] + if (modelName.includes('o3-mini')) return openAIModelOptions['o3-mini'] + if (modelName.includes('gpt-4o')) return openAIModelOptions['gpt-4o'] + throw new Error(modelNotRecognizedErrorMessage(modelName, 'openAI')) + } +} + +// ---------------- ANTHROPIC ---------------- +const anthropicModelOptions = { + "claude-3-5-sonnet-20241022": { + contextWindow: 200_000, + cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + supportsFIM: false, + supportsSystemMessage: 'separated', + supportsTools: 'anthropic-style', + supportsReasoning: false, + + }, + "claude-3-5-haiku-20241022": { + contextWindow: 200_000, + cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, + supportsFIM: false, + supportsSystemMessage: 'separated', + supportsTools: 'anthropic-style', + supportsReasoning: false, + }, + "claude-3-opus-20240229": { + contextWindow: 200_000, + cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, + supportsFIM: false, + supportsSystemMessage: 'separated', + supportsTools: 'anthropic-style', + supportsReasoning: false, + }, + "claude-3-sonnet-20240229": { + contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + supportsFIM: false, + supportsSystemMessage: 'separated', + supportsTools: 'anthropic-style', + supportsReasoning: false, + } +} as const + +const anthropicSettings: ProviderSettings = { + modelOptions: anthropicModelOptions, + modelOptionsFallback: (modelName) => { + throw new Error(modelNotRecognizedErrorMessage(modelName, 'anthropic')) + } +} + + +// ---------------- XAI ---------------- +const XAIModelOptions = { + "grok-2-latest": { + contextWindow: 131_072, + cost: { input: 2.00, output: 10.00 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + }, +} as const + +const XAISettings: ProviderSettings = { + modelOptions: XAIModelOptions, + modelOptionsFallback: (modelName) => { + throw new Error(modelNotRecognizedErrorMessage(modelName, 'xAI')) + } +} + + + +const modelSettingsOfProvider: ModelSettingsOfProvider = { + openAI: openAISettings, + anthropic: anthropicSettings, + xAI: XAISettings, + gemini: { + modelOptions: { + } - } -} + }, + googleVertex: { + }, + microsoftAzure: { -const openAIProviderSettings: ProviderSettings = { - - thinkingFormat: '', - - toolsFormat: '', - - FIMFormat: '', - - modelOptions: { - 'o1': { - contextWindow: 128_000, - cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, - supportsFIM: false, - supportsTools: false, - supportsSystemMessage: 'developer-role', - }, - 'o3-mini': { - contextWindow: 200_000, - cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, - supportsFIM: false, - supportsTools: false, - supportsSystemMessage: 'developer-role', - }, - 'gpt-4o': { - contextWindow: 128_000, - cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, - supportsFIM: false, - supportsTools: 'openai-style', - supportsSystemMessage: 'system-role', - }, - } - -} - - - - - -const anthropicProviderSettings: ProviderSettings = { - thinkingFormat: '', - - toolsFormat: '', - - FIMFormat: '', - - modelOptions: { - "claude-3-5-sonnet-20241022": { - contextWindow: 200_000, - cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - - }, - "claude-3-5-haiku-20241022": { - contextWindow: 200_000, - cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - }, - "claude-3-opus-20240229": { - contextWindow: 200_000, - cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - }, - "claude-3-sonnet-20240229": { - contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', + }, + openRouter: { + providerReasoningOptions: { + // reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models + input: { includeInPayload: { include_reasoning: true } }, + output: { nameOfFieldInDelta: 'reasoning' }, } - } -} - - - -const grokProviderSettings: ProviderSettings = { - thinkingFormat: '', - - toolsFormat: '', - - FIMFormat: '', - - modelOptions: { - "grok-2-latest": { - contextWindow: 131_072, - cost: { input: 2.00, output: 10.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', + }, + vLLM: { + providerReasoningOptions: { + // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions + output: { nameOfFieldInDelta: 'reasoning_content' }, + } + }, + deepseek: { + providerReasoningOptions: { + // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model + output: { nameOfFieldInDelta: 'reasoning_content' }, }, - } + }, + ollama: { + providerReasoningOptions: { + // reasoning: we need to filter out reasoning tags manually + output: { needsManualParse: true }, + }, + }, + openAICompatible: { + }, + mistral: { + }, + groq: { + }, + + + +} as const satisfies ModelSettingsOfProvider + + +const modelOptionsOfProvider = (providerName: ProviderName, modelName: string) => { + const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] + if (modelName in modelOptions) return modelOptions[modelName] + return modelOptionsFallback(modelName) } - - type InternalCommonMessageParams = { aiInstructions: string; onText: OnText; @@ -234,30 +320,94 @@ const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex) => { } -const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName }) => { +const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => { + const commonPayloadOpts: ClientOptions = { + dangerouslyAllowBrowser: true, + ...includeInPayload, + } if (providerName === 'openAI') { const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, }) + return new OpenAI({ apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } else if (providerName === 'ollama') { const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, }) + return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts }) } else if (providerName === 'vLLM') { const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, }) + return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts }) } - else throw new Error(`Invalid providerName ${providerName}`) + else if (providerName === 'openRouter') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: thisConfig.apiKey, + defaultHeaders: { + 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. + 'X-Title': 'Void', // Optional. Shows in rankings on openrouter.ai. + }, + ...commonPayloadOpts, + }) + } + else if (providerName === 'gemini') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + } + else if (providerName === 'deepseek') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + } + else if (providerName === 'openAICompatible') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + } + else if (providerName === 'mistral') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + } + else if (providerName === 'groq') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + } + else if (providerName === 'xAI') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + } + + else throw new Error(`Void providerName was invalid: ${providerName}.`) } -export const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { - const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: '', supportsTools: '', }) + + +const manualParseOnText = ( + providerName: ProviderName, + modelName: string, + onText_: OnText +): OnText => { + return onText_ +} + + +const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + const { + supportsReasoning: modelSupportsReasoning, + supportsSystemMessage, + supportsTools, + } = modelOptionsOfProvider(providerName, modelName) + + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined + const includeInPayload = modelSupportsReasoning ? {} : modelSettingsOfProvider[providerName].providerReasoningOptions?.input?.includeInPayload || {} + const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} - const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) + const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj } + const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].providerReasoningOptions?.output ?? {} + if (needsManualReasoningParse) onText = manualParseOnText(providerName, modelName, onText) + + let fullReasoning = '' let fullText = '' const toolCallOfIndex: ToolCallOfIndex = {} openai.chat.completions @@ -275,10 +425,18 @@ export const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinal toolCallOfIndex[index].id = tool.id ?? '' } // message - let newText = '' - newText += chunk.choices[0]?.delta?.content ?? '' + const newText = chunk.choices[0]?.delta?.content ?? '' fullText += newText - onText({ newText, fullText }) + + // reasoning + let newReasoning = '' + if (nameOfReasoningFieldInDelta) { + // @ts-ignore + newReasoning = (chunk.choices[0]?.delta?.[nameOfFieldInDelta] || '') + '' + fullReasoning += newReasoning + } + + onText({ newText, fullText, newReasoning, fullReasoning }) } onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) }); }) @@ -290,7 +448,7 @@ export const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinal } -export const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal) => { +const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OpenAIModel[] }) => { onSuccess_({ models }) } @@ -318,8 +476,9 @@ export const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: on } + // ------------ OPENAI ------------ -export const sendOpenAIChat = (params: SendChatParams_Internal) => { +const sendOpenAIChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) } @@ -345,25 +504,31 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[ }).filter(t => !!t) } -export const sendAnthropicChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { - const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', }) +const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + const { + // supportsReasoning: modelSupportsReasoning, + supportsSystemMessage, + supportsTools, + contextWindow, + } = modelOptionsOfProvider(providerName, modelName) + + const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) const thisConfig = settingsOfProvider.anthropic const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); const tools = ((tools_?.length ?? 0) !== 0) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined - const maxTokens = ; const stream = anthropic.messages.stream({ system: separateSystemMessageStr, messages: messages, model: modelName, - max_tokens: maxTokens, + max_tokens: contextWindow, tools: tools, tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time }) // when receive text stream.on('text', (newText, fullText) => { - onText({ newText, fullText }) + onText({ newText, fullText, newReasoning: '', fullReasoning: '' }) }) // when we get the final message on this stream (or when error/fail) stream.on('finalMessage', (response) => { @@ -377,7 +542,7 @@ export const sendAnthropicChat = ({ messages: messages_, onText, onFinalMessage, else { onError({ message: error + '', fullError: error }) } }) _setAborter(() => stream.controller.abort()) -}; +} // // in future, can do tool_use streaming in anthropic, but it's pretty fast even without streaming... // const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {} @@ -396,6 +561,16 @@ export const sendAnthropicChat = ({ messages: messages_, onText, onFinalMessage, // }) +// ------------ XAI ------------ +const sendXAIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ GEMINI ------------ +const sendGeminiAPIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) +} + // ------------ OLLAMA ------------ const newOllamaSDK = ({ endpoint }: { endpoint: string }) => { // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in @@ -404,7 +579,7 @@ const newOllamaSDK = ({ endpoint }: { endpoint: string }) => { return ollama } -export const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal) => { +const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => { onSuccess_({ models }) } @@ -428,7 +603,7 @@ export const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, set } } -export const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { +const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { const thisConfig = settingsOfProvider.ollama const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) @@ -461,58 +636,141 @@ export const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfPro // ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat! -export const sendOllamaChat = (params: SendChatParams_Internal) => { +const sendOllamaChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) - // TODO!!! filter out reasoning tags... -} - - - -// ------------ OPENROUTER ------------ -export const sendOpenRouterFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { - // TODO!!! -} - -export const sendOpenRouterChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { - // reasoning: response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models - // } // ------------ OPENAI-COMPATIBLE ------------ -export const openAICompatibleList = async (params: ListParams_Internal) => { - return _openaiCompatibleList(params) -} - // TODO!!! FIM // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -export const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => { +const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) } +// ------------ OPENROUTER ------------ +const sendOpenRouterChat = (params: SendChatParams_Internal) => { + _sendOpenAICompatibleChat(params) +} + // ------------ VLLM ------------ - -// TODO!!! FIM +const vLLMList = async (params: ListParams_Internal) => { + return _openaiCompatibleList(params) +} +const sendVLLMFIM = (params: SendFIMParams_Internal) => { + // TODO!!! +} // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -export const sendVLLMChat = (params: SendChatParams_Internal) => { +const sendVLLMChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) - // reasoning: response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions } - // ------------ DEEPSEEK API ------------ -export const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => { +const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ MISTRAL ------------ +const sendMistralAPIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ GROQ ------------ +const sendGroqAPIChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) - // reasoning: response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model } -// ------------ GEMINI ------------ -// ------------ MISTRAL ------------ -// ------------ GROQ ------------ -// ------------ GROK ------------ + + +/* +FIM: + +qwen2.5-coder https://ollama.com/library/qwen2.5-coder/blobs/e94a8ecb9327 +<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> + +codestral https://ollama.com/library/codestral/blobs/51707752a87c +[SUFFIX]{{ .Suffix }}[PREFIX] {{ .Prompt }} + +deepseek-coder-v2 https://ollama.com/library/deepseek-coder-v2/blobs/22091531faf0 +<|fim▁begin|>{{ .Prompt }}<|fim▁hole|>{{ .Suffix }}<|fim▁end|> + +starcoder2 https://ollama.com/library/starcoder2/blobs/3b190e68fefe + + +{{ .Prompt }}{{ .Suffix }} +<|end_of_text|> + +codegemma https://ollama.com/library/codegemma:2b/blobs/48d9a8140749 +<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> + +*/ +type CallFnOfProvider = { + [providerName in ProviderName]: { + sendChat: (params: SendChatParams_Internal) => void; + sendFIM: ((params: SendFIMParams_Internal) => void) | null; + list: ((params: ListParams_Internal) => void) | null; + } +} +export const sendLLMMessageToProviderImplementation = { + openAI: { + sendChat: sendOpenAIChat, + sendFIM: null, + list: null, + }, + anthropic: { + sendChat: sendAnthropicChat, + sendFIM: null, + list: null, + }, + xAI: { + sendChat: sendXAIChat, + sendFIM: null, + list: null, + }, + gemini: { + sendChat: sendGeminiAPIChat, + sendFIM: null, + list: null, + }, + ollama: { + sendChat: sendOllamaChat, + sendFIM: sendOllamaFIM, + list: ollamaList, + }, + openAICompatible: { + sendChat: sendOpenAICompatibleChat, + sendFIM: null, + list: null, + }, + openRouter: { + sendChat: sendOpenRouterChat, + sendFIM: null, + list: null, + }, + vLLM: { + sendChat: sendVLLMChat, + sendFIM: sendVLLMFIM, + list: vLLMList, + }, + deepseek: { + sendChat: sendDeepSeekAPIChat, + sendFIM: null, + list: null, + }, + groq: { + sendChat: sendGroqAPIChat, + sendFIM: null, + list: null, + }, + mistral: { + sendChat: sendMistralAPIChat, + sendFIM: null, + list: null, + }, +} satisfies CallFnOfProvider diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 3cef5327..1d388338 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -266,6 +266,31 @@ const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatM +/* +Gemini has this, but they're openai-compat so we don't need to implement this +gemini request: +{ "role": "assistant", + "content": null, + "function_call": { + "name": "get_weather", + "arguments": { + "latitude": 48.8566, + "longitude": 2.3522 + } + } +} + +gemini response: +{ "role": "assistant", + "function_response": { + "name": "get_weather", + "response": { + "temperature": "15°C", + "condition": "Cloudy" + } + } +} +*/ @@ -297,29 +322,3 @@ export const prepareMessages = ({ } as const } - -/* -ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) -gemini request: -{ "role": "assistant", - "content": null, - "function_call": { - "name": "get_weather", - "arguments": { - "latitude": 48.8566, - "longitude": 2.3522 - } - } -} - -gemini response: -{ "role": "assistant", - "function_response": { - "name": "get_weather", - "response": { - "temperature": "15°C", - "condition": "Cloudy" - } - } -} -*/ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 1c9ac21d..90deffe2 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -6,7 +6,7 @@ import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; -import { sendAnthropicChat, sendOpenAIChat } from './MODELS.js'; +import { sendLLMMessageToProviderImplementation } from './MODELS.js'; export const sendLLMMessage = ({ @@ -56,9 +56,10 @@ export const sendLLMMessage = ({ let _setAborter = (fn: () => void) => { _aborter = fn } let _didAbort = false - const onText: OnText = ({ newText, fullText }) => { + const onText: OnText = (params) => { + const { fullText } = params if (_didAbort) return - onText_({ newText, fullText }) + onText_(params) _fullTextSoFar = fullText } @@ -93,29 +94,27 @@ export const sendLLMMessage = ({ else if (messagesType === 'FIMMessage') captureLLMEvent(`${loggingName} - Sending FIM`, {}) // TODO!!! add more metrics + try { - switch (providerName) { - case 'anthropic': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' }) - else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); - break; - case 'openAI': - case 'openRouter': - case 'deepseek': - case 'openAICompatible': - case 'mistral': - case 'ollama': - case 'vLLM': - case 'groq': - case 'gemini': - case 'xAI': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI API FIM' }) - else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); - break; - default: - onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) - break; + const implementation = sendLLMMessageToProviderImplementation[providerName] + if (!implementation) { + onError({ message: `Error: Provider "${providerName}" not recognized.`, fullError: null }) + return } + const { sendFIM, sendChat } = implementation + if (messagesType === 'chatMessages') { + sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }) + return + } + if (messagesType === 'FIMMessage') { + if (sendFIM) { + sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions }) + return + } + onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null }) + return + } + onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null }) } catch (error) { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts index 929c85e4..d2bceb4c 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts @@ -8,29 +8,42 @@ import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/llmMessageTypes.js'; import { sendLLMMessage } from './llmMessage/sendLLMMessage.js' import { IMetricsService } from '../common/metricsService.js'; -import { ollamaList } from './llmMessage/MODELS.js'; +import { sendLLMMessageToProviderImplementation } from './llmMessage/MODELS.js'; // NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it export class LLMMessageChannel implements IServerChannel { + // sendLLMMessage - private readonly _onText_llm = new Emitter(); - private readonly _onFinalMessage_llm = new Emitter(); - private readonly _onError_llm = new Emitter(); + private readonly llmMessageEmitters = { + onText: new Emitter(), + onFinalMessage: new Emitter(), + onError: new Emitter(), + } - // abort - private readonly _abortRefOfRequestId_llm: Record = {} + // aborters for above + private readonly abortRefOfRequestId: Record = {} - // ollamaList - private readonly _onSuccess_ollama = new Emitter>(); - private readonly _onError_ollama = new Emitter>(); - // openaiCompatibleList - private readonly _onSuccess_openAICompatible = new Emitter>(); - private readonly _onError_openAICompatible = new Emitter>(); + // list + private readonly listEmitters = { + ollama: { + success: new Emitter>(), + error: new Emitter>(), + }, + vLLM: { + success: new Emitter>(), + error: new Emitter>(), + } + } satisfies { + [providerName: string]: { + success: Emitter>, + error: Emitter>, + } + } // stupidly, channels can't take in @IService constructor( @@ -39,30 +52,17 @@ export class LLMMessageChannel implements IServerChannel { // browser uses this to listen for changes listen(_: unknown, event: string): Event { - if (event === 'onText_llm') { - return this._onText_llm.event; - } - else if (event === 'onFinalMessage_llm') { - return this._onFinalMessage_llm.event; - } - else if (event === 'onError_llm') { - return this._onError_llm.event; - } - else if (event === 'onSuccess_ollama') { - return this._onSuccess_ollama.event; - } - else if (event === 'onError_ollama') { - return this._onError_ollama.event; - } - else if (event === 'onSuccess_openAICompatible') { - return this._onSuccess_openAICompatible.event; - } - else if (event === 'onError_openAICompatible') { - return this._onError_openAICompatible.event; - } - else { - throw new Error(`Event not found: ${event}`); - } + // text + if (event === 'onText_sendLLMMessage') return this.llmMessageEmitters.onText.event; + else if (event === 'onFinalMessage_sendLLMMessage') return this.llmMessageEmitters.onFinalMessage.event; + else if (event === 'onError_sendLLMMessage') return this.llmMessageEmitters.onError.event; + // list + else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event; + else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event; + else if (event === 'onSuccess_list_vLLM') return this.listEmitters.vLLM.success.event; + else if (event === 'onError_list_vLLM') return this.listEmitters.vLLM.error.event; + + else throw new Error(`Event not found: ${event}`); } // browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages) @@ -77,8 +77,8 @@ export class LLMMessageChannel implements IServerChannel { else if (command === 'ollamaList') { this._callOllamaList(params) } - else if (command === 'openAICompatibleList') { - this._callOpenAICompatibleList(params) + else if (command === 'vLLMList') { + this._callVLLMList(params) } else { throw new Error(`Void sendLLM: command "${command}" not recognized.`) @@ -93,47 +93,50 @@ export class LLMMessageChannel implements IServerChannel { private async _callSendLLMMessage(params: MainSendLLMMessageParams) { const { requestId } = params; - if (!(requestId in this._abortRefOfRequestId_llm)) - this._abortRefOfRequestId_llm[requestId] = { current: null } + if (!(requestId in this.abortRefOfRequestId)) + this.abortRefOfRequestId[requestId] = { current: null } const mainThreadParams: SendLLMMessageParams = { ...params, - onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); }, - onFinalMessage: ({ fullText, toolCalls }) => { this._onFinalMessage_llm.fire({ requestId, fullText, toolCalls }); }, - onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); }, - abortRef: this._abortRefOfRequestId_llm[requestId], + onText: (p) => { this.llmMessageEmitters.onText.fire({ requestId, ...p }); }, + onFinalMessage: (p) => { this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p }); }, + onError: (p) => { console.log('sendLLM: firing err'); this.llmMessageEmitters.onError.fire({ requestId, ...p }); }, + abortRef: this.abortRefOfRequestId[requestId], } sendLLMMessage(mainThreadParams, this.metricsService); } - private _callAbort(params: MainLLMMessageAbortParams) { - const { requestId } = params; - if (!(requestId in this._abortRefOfRequestId_llm)) return - this._abortRefOfRequestId_llm[requestId].current?.() - delete this._abortRefOfRequestId_llm[requestId] - } - - private _callOllamaList(params: MainModelListParams) { - const { requestId } = params; - + _callOllamaList = (params: MainModelListParams) => { + const { requestId } = params + const emitters = this.listEmitters.ollama const mainThreadParams: ModelListParams = { ...params, - onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); }, - onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); }, + onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); }, + onError: (p) => { emitters.error.fire({ requestId, ...p }); }, } - ollamaList(mainThreadParams) + sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams) } - private _callOpenAICompatibleList(params: MainModelListParams) { - const { requestId } = params; - - const mainThreadParams: ModelListParams = { + _callVLLMList = (params: MainModelListParams) => { + const { requestId } = params + const emitters = this.listEmitters.vLLM + const mainThreadParams: ModelListParams = { ...params, - onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); }, - onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); }, + onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); }, + onError: (p) => { emitters.error.fire({ requestId, ...p }); }, } - openaiCompatibleList(mainThreadParams) + sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams) } + + + + private _callAbort(params: MainLLMMessageAbortParams) { + const { requestId } = params; + if (!(requestId in this.abortRefOfRequestId)) return + this.abortRefOfRequestId[requestId].current?.() + delete this.abortRefOfRequestId[requestId] + } + } From 3ae8f756410f5cc56054f196b9e2193f1e616425 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 04:01:00 -0800 Subject: [PATCH 88/92] remove mistral, finish(?) models! --- package-lock.json | 13 +- package.json | 2 +- .../browser/helpers/extractCodeFromResult.ts | 94 +++ .../react/src/void-settings-tsx/Settings.tsx | 2 +- .../void/common/voidSettingsService.ts | 39 +- .../contrib/void/common/voidSettingsTypes.ts | 88 +- .../void/electron-main/llmMessage/MODELS.ts | 755 +++++++++++------- .../llmMessage/preprocessLLMMessages.ts | 26 +- 8 files changed, 686 insertions(+), 333 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc045891..d29248c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "node-pty": "1.1.0-beta21", "ollama": "^0.5.11", "open": "^8.4.2", - "openai": "^4.76.1", + "openai": "^4.85.4", "posthog-node": "^4.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -17079,9 +17079,10 @@ } }, "node_modules/openai": { - "version": "4.77.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.77.0.tgz", - "integrity": "sha512-WWacavtns/7pCUkOWvQIjyOfcdr9X+9n9Vvb0zFeKVDAqwCMDHB+iSr24SVaBAhplvSG6JrRXFpcNM9gWhOGIw==", + "version": "4.85.4", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.85.4.tgz", + "integrity": "sha512-Nki51PBSu+Aryo7WKbdXvfm0X/iKkQS2fq3O0Uqb/O3b4exOZFid2te1BZ52bbO5UwxQZ5eeHJDCTqtrJLPw0w==", + "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -17095,9 +17096,13 @@ "openai": "bin/cli" }, "peerDependencies": { + "ws": "^8.18.0", "zod": "^3.23.8" }, "peerDependenciesMeta": { + "ws": { + "optional": true + }, "zod": { "optional": true } diff --git a/package.json b/package.json index a4ee38bb..b99ca9dc 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "node-pty": "1.1.0-beta21", "ollama": "^0.5.11", "open": "^8.4.2", - "openai": "^4.76.1", + "openai": "^4.85.4", "posthog-node": "^4.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 806676da..297d82b6 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { OnText } from '../../common/llmMessageTypes.js' import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js' class SurroundingsRemover { @@ -240,3 +241,96 @@ export const extractSearchReplaceBlocks = (str: string) => { }) } } + + + + + + + + + + +export const extractReasoningFromText = ( + onText_: OnText, + thinkTags: [string, string], +): OnText => { + + let latestAddIdx = 0 // exclusive + let foundTag1 = false + let foundTag2 = false + + let fullText = '' + let fullReasoning = '' + + const onText: OnText = ({ newText: newText_, fullText: fullText_ }) => { + // abcdefghi + // | + // until found the first think tag, keep adding to fullText + if (!foundTag1) { + const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0]) + if (endsWithTag1) { + // wait until we get the full tag or know more + return + } + // if found the first tag + const tag1Index = fullText_.lastIndexOf(thinkTags[0]) + if (tag1Index !== -1) { + foundTag1 = true + const newText = fullText.substring(latestAddIdx, tag1Index) + const newReasoning = fullText.substring(tag1Index + thinkTags[0].length, Infinity) + + fullText += newText + fullReasoning += newReasoning + latestAddIdx += newText.length + newReasoning.length + onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning }) + return + } + + // add the text to fullText + const newText = fullText.substring(latestAddIdx, Infinity) + fullText += newText + latestAddIdx += newText.length + onText_({ newText, fullText, newReasoning: '', fullReasoning }) + return + } + // at this point, we found + + // until found the second think tag, keep adding to fullReasoning + if (!foundTag2) { + const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1]) + if (endsWithTag2) { + // wait until we get the full tag or know more + return + } + // if found the second tag + const tag2Index = fullText_.lastIndexOf(thinkTags[1]) + if (tag2Index !== -1) { + foundTag2 = true + const newReasoning = fullText.substring(latestAddIdx, tag2Index) + const newText = fullText.substring(tag2Index + thinkTags[1].length, Infinity) + + fullText += newText + fullReasoning += newReasoning + latestAddIdx += newText.length + newReasoning.length + onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning }) + return + } + + // add the text to fullReasoning + const newReasoning = fullText.substring(latestAddIdx, Infinity) + fullReasoning += newReasoning + latestAddIdx += newReasoning.length + onText_({ newText: '', fullText, newReasoning, fullReasoning }) + return + } + // at this point, we found + + fullText += newText_ + const newText = fullText.substring(latestAddIdx, Infinity) + latestAddIdx += newText.length + onText_({ newText, fullText, newReasoning: '', fullReasoning }) + } + + return onText +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index a6aec380..e2056fbf 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -415,7 +415,7 @@ export const FeaturesTab = () => {
    - + {/* TODO we should create UI for downloading models without user going into terminal */} diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 7a35c678..be3f6689 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IMetricsService } from './metricsService.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings } from './voidSettingsTypes.js'; const STORAGE_KEY = 'void.settingsServiceStorage' @@ -32,8 +32,6 @@ type SetGlobalSettingFn = (settingName: T, newVal export type ModelOption = { name: string, selection: ModelSelection } - - export type VoidSettingsState = { readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature @@ -172,9 +170,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS) ...{ deepseek: defaultSettingsOfProvider.deepseek }, - // A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS) - ...{ mistral: defaultSettingsOfProvider.mistral }, - // A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS) ...{ xAI: defaultSettingsOfProvider.xAI }, @@ -295,19 +290,35 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } + private _updatedModelsAfterAutodetection = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { + const { existingModels } = options + + const existingModelsMap: Record = {} + for (const existingModel of existingModels) { + existingModelsMap[existingModel.modelName] = existingModel + } + + const newDefaultModels = defaultModelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: true, + isHidden: !!existingModelsMap[modelName]?.isHidden, + })) + + return [ + ...newDefaultModels, // swap out all the default models for the new default models + ...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models + ] + } + + setAutodetectedModels(providerName: ProviderName, autodetectedModelNames: string[], logging: object) { const { models } = this.state.settingsOfProvider[providerName] const oldModelNames = models.map(m => m.modelName) - - const newDefaultModels = modelInfoOfAutodetectedModelNames(autodetectedModelNames, { existingModels: models }) - const newModels = [ - ...newDefaultModels, // swap out all the default models for the new default models - ...models.filter(m => !m.isDefault), // keep any non-default (custom) models - ] - + const newModels = this._updatedModelsAfterAutodetection(autodetectedModelNames, { existingModels: models }) this.setSettingOfProvider(providerName, 'models', newModels) // if the models changed, log it @@ -341,7 +352,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { if (existingIdx !== -1) return // if exists, do nothing const newModels = [ ...models, - { ...developerInfoOfModelName(modelName), modelName, isDefault: false, isHidden: false } + { modelName, isDefault: false, isHidden: false } ] this.setSettingOfProvider(providerName, 'models', newModels) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index fb387bc1..4111b53b 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -4,7 +4,6 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { defaultModelsOfProvider } from '../electron-main/llmMessage/MODELS.js'; import { VoidSettingsState } from './voidSettingsService.js' @@ -40,14 +39,70 @@ export const defaultProviderSettings = { groq: { apiKey: '', }, - mistral: { - apiKey: '' - }, xAI: { apiKey: '' }, } as const + + + +export const defaultModelsOfProvider = { + openAI: [ // https://platform.openai.com/docs/models/gp + 'o1', + 'o3-mini', + 'o1-mini', + 'gpt-4o', + 'gpt-4o-mini', + ], + anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models + 'claude-3-5-sonnet-latest', + 'claude-3-5-haiku-latest', + 'claude-3-opus-latest', + ], + xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1 + 'grok-2-latest', + 'grok-3-latest', + ], + gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini + 'gemini-2.0-flash', + 'gemini-1.5-flash', + 'gemini-1.5-pro', + 'gemini-1.5-flash-8b', + 'gemini-2.0-flash-thinking-exp', + ], + deepseek: [ // https://api-docs.deepseek.com/quick_start/pricing + 'deepseek-chat', + 'deepseek-reasoner', + ], + ollama: [ // autodetected + ], + vLLM: [ // autodetected + ], + openRouter: [ // https://openrouter.ai/models + 'anthropic/claude-3.5-sonnet', + 'deepseek/deepseek-r1', + 'mistralai/codestral-2501', + 'qwen/qwen2.5-vl-72b-instruct:free', + ], + groq: [ // https://console.groq.com/docs/models + 'llama-3.3-70b-versatile', + 'llama-3.1-8b-instant', + 'qwen-2.5-coder-32b', // preview mode (experimental) + ], + // not supporting mistral right now- it's last on Void usage, and a huge pain to set up since it's nonstandard (it supports codestral FIM but it's on v1/fim/completions, etc) + // mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ + // 'codestral-latest', + // 'mistral-large-latest', + // 'ministral-3b-latest', + // 'ministral-8b-latest', + // ], + openAICompatible: [], // fallback +} as const satisfies Record + + + + export type ProviderName = keyof typeof defaultProviderSettings export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[] @@ -139,11 +194,6 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn title: 'Groq.com API', } } - else if (providerName === 'mistral') { - return { - title: 'Mistral API', - } - } else if (providerName === 'xAI') { return { title: 'xAI API', @@ -173,10 +223,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key providerName === 'gemini' ? 'key...' : providerName === 'groq' ? 'gsk_key...' : - providerName === 'mistral' ? 'key...' : - providerName === 'openAICompatible' ? 'sk-key...' : - providerName === 'xAI' ? 'xai-key...' : - '', + providerName === 'openAICompatible' ? 'sk-key...' : + providerName === 'xAI' ? 'xai-key...' : + '', subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : @@ -184,10 +233,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' : providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : - providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' : - providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : - providerName === 'openAICompatible' ? undefined : - '', + providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : + providerName === 'openAICompatible' ? undefined : + '', isPasswordField: true, } } @@ -271,12 +319,6 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.gemini), _didFillInProviderSettings: undefined, }, - mistral: { - ...defaultCustomSettings, - ...defaultProviderSettings.mistral, - ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral), - _didFillInProviderSettings: undefined, - }, xAI: { ...defaultCustomSettings, ...defaultProviderSettings.xAI, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index ce0d0537..d68408cd 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -4,67 +4,15 @@ *--------------------------------------------------------------------------------------*/ import OpenAI, { ClientOptions } from 'openai'; -import { Model as OpenAIModel } from 'openai/resources/models.js'; -import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; -import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; -import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; -import { prepareMessages } from './preprocessLLMMessages.js'; import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; - - -export const defaultModelsOfProvider = { - anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models - 'claude-3-5-sonnet-latest', - 'claude-3-5-haiku-latest', - 'claude-3-opus-latest', - ], - openAI: [ // https://platform.openai.com/docs/models/gp - 'o1', - 'o1-mini', - 'o3-mini', - 'gpt-4o', - 'gpt-4o-mini', - ], - deepseek: [ // https://platform.openai.com/docs/models/gp - 'deepseek-chat', - 'deepseek-reasoner', - ], - ollama: [], - vLLM: [], - openRouter: [], - openAICompatible: [], - gemini: [ - 'gemini-1.5-flash', - 'gemini-1.5-pro', - 'gemini-1.5-flash-8b', - 'gemini-2.0-flash-exp', - 'gemini-2.0-flash-thinking-exp-1219', - 'learnlm-1.5-pro-experimental' - ], - groq: [ // https://console.groq.com/docs/models - "llama3-70b-8192", - "llama-3.3-70b-versatile", - "llama-3.1-8b-instant", - "gemma2-9b-it", - "mixtral-8x7b-32768" - ], - mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ - "codestral-latest", - "open-codestral-mamba", - "open-mistral-nemo", - "mistral-large-latest", - "pixtral-large-latest", - "ministral-3b-latest", - "ministral-8b-latest", - "mistral-small-latest", - ], - xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1 - 'grok-3-latest', - 'grok-2-latest', - ], -} satisfies Record +import { Model as OpenAIModel } from 'openai/resources/models.js'; +import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; +import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; +import { defaultProviderSettings, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; +import { extractReasoningFromText } from '../../browser/helpers/extractCodeFromResult.js'; @@ -78,10 +26,13 @@ type ModelOptions = { } supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; supportsTools: false | 'anthropic-style' | 'openai-style'; - supportsFIM: false | 'TODO_FIM_FORMAT'; + supportsFIM: boolean; - supportsReasoning: boolean; // not whether it reasons, but whether it outputs reasoning tokens - manualMatchReasoningTokens?: [string, string]; // reasoning tokens if it's an OSS model + supportsReasoningOutput: false | { + // you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing) + // if it's open source, put the think tags here so we parse them out in e.g. ollama + openSourceThinkTags?: [string, string] + }; } type ProviderReasoningOptions = { @@ -95,9 +46,9 @@ type ProviderReasoningOptions = { } type ProviderSettings = { - providerReasoningOptions?: ProviderReasoningOptions; + ifSupportsReasoningOutput?: ProviderReasoningOptions; modelOptions: { [key: string]: ModelOptions }; - modelOptionsFallback: (modelName: string) => ModelOptions; // allowed to throw error if modeName is totally invalid + modelOptionsFallback: (modelName: string) => (ModelOptions & { modelName: string }) | null; } @@ -107,172 +58,446 @@ type ModelSettingsOfProvider = { +// type DefaultModels = typeof defaultModelsOfProvider[T][number] +// type AssertModelsIncluded< +// T extends ProviderName, +// Options extends Record +// > = Exclude, keyof Options> extends never +// ? true +// : ["Missing models for", T, Exclude, keyof Options>]; +// const assertOpenAI: AssertModelsIncluded<'openAI', typeof openAIModelOptions> = true; -const modelNotRecognizedErrorMessage = (modelName: string, providerName: ProviderName) => `Void could not find a model matching ${modelName} for ${displayInfoOfProviderName(providerName).title}.` - +const modelOptionDefaults: ModelOptions = { + contextWindow: 32_000, + cost: { input: 0, output: 0 }, + supportsSystemMessage: false, + supportsTools: false, + supportsFIM: false, + supportsReasoningOutput: false, +} // ---------------- OPENAI ---------------- -const openAIModelOptions = { - "o1": { +const openAIModelOptions = { // https://platform.openai.com/docs/pricing + 'o1': { contextWindow: 128_000, cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', - supportsReasoning: false, + supportsReasoningOutput: false, }, - "o3-mini": { + 'o3-mini': { contextWindow: 200_000, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', - supportsReasoning: false, + supportsReasoningOutput: false, }, - "gpt-4o": { + 'gpt-4o': { contextWindow: 128_000, cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, supportsFIM: false, supportsTools: 'openai-style', supportsSystemMessage: 'system-role', - supportsReasoning: false, + supportsReasoningOutput: false, }, -} as const + 'o1-mini': { + contextWindow: 128_000, + cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + supportsFIM: false, + supportsTools: false, + supportsSystemMessage: false, // does not support any system + supportsReasoningOutput: false, + }, + 'gpt-4o-mini': { + contextWindow: 128_000, + cost: { input: 0.15, cache_read: 0.075, output: 0.60, }, + supportsFIM: false, + supportsTools: 'openai-style', + supportsSystemMessage: 'system-role', // ?? + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: ModelOptions } + const openAISettings: ProviderSettings = { modelOptions: openAIModelOptions, modelOptionsFallback: (modelName) => { - if (modelName.includes('o1')) return openAIModelOptions['o1'] - if (modelName.includes('o3-mini')) return openAIModelOptions['o3-mini'] - if (modelName.includes('gpt-4o')) return openAIModelOptions['gpt-4o'] - throw new Error(modelNotRecognizedErrorMessage(modelName, 'openAI')) + let fallbackName: keyof typeof openAIModelOptions | null = null + if (modelName.includes('o1')) { fallbackName = 'o1' } + if (modelName.includes('o3-mini')) { fallbackName = 'o3-mini' } + if (modelName.includes('gpt-4o')) { fallbackName = 'gpt-4o' } + if (fallbackName) return { modelName: fallbackName, ...openAIModelOptions[fallbackName] } + return null } } // ---------------- ANTHROPIC ---------------- const anthropicModelOptions = { - "claude-3-5-sonnet-20241022": { + 'claude-3-5-sonnet-20241022': { contextWindow: 200_000, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, - + supportsReasoningOutput: false, }, - "claude-3-5-haiku-20241022": { + 'claude-3-5-haiku-20241022': { contextWindow: 200_000, cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + supportsReasoningOutput: false, }, - "claude-3-opus-20240229": { + 'claude-3-opus-20240229': { contextWindow: 200_000, cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + supportsReasoningOutput: false, }, - "claude-3-sonnet-20240229": { + 'claude-3-sonnet-20240229': { // no point of using this, but including this for people who put it in contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + supportsReasoningOutput: false, } -} as const +} as const satisfies { [s: string]: ModelOptions } const anthropicSettings: ProviderSettings = { modelOptions: anthropicModelOptions, modelOptionsFallback: (modelName) => { - throw new Error(modelNotRecognizedErrorMessage(modelName, 'anthropic')) + let fallbackName: keyof typeof anthropicModelOptions | null = null + if (modelName.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022' + if (modelName.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022' + if (modelName.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229' + if (fallbackName) return { modelName: fallbackName, ...anthropicModelOptions[fallbackName] } + return null } } // ---------------- XAI ---------------- -const XAIModelOptions = { - "grok-2-latest": { +const xAIModelOptions = { + 'grok-2-latest': { contextWindow: 131_072, cost: { input: 2.00, output: 10.00 }, supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + supportsReasoningOutput: false, }, -} as const +} as const satisfies { [s: string]: ModelOptions } -const XAISettings: ProviderSettings = { - modelOptions: XAIModelOptions, +const xAISettings: ProviderSettings = { + modelOptions: xAIModelOptions, modelOptionsFallback: (modelName) => { - throw new Error(modelNotRecognizedErrorMessage(modelName, 'xAI')) + let fallbackName: keyof typeof xAIModelOptions | null = null + if (modelName.includes('grok-2')) fallbackName = 'grok-2-latest' + if (fallbackName) return { modelName: fallbackName, ...xAIModelOptions[fallbackName] } + return null } } +// ---------------- GEMINI ---------------- +const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing + 'gemini-2.0-flash': { + contextWindow: 1_048_576, + cost: { input: 0.10, output: 0.40 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini + supportsReasoningOutput: false, + }, + 'gemini-2.0-flash-lite-preview-02-05': { + contextWindow: 1_048_576, + cost: { input: 0.075, output: 0.30 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'gemini-1.5-flash': { + contextWindow: 1_048_576, + cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'gemini-1.5-pro': { + contextWindow: 2_097_152, + cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'gemini-1.5-flash-8b': { + contextWindow: 1_048_576, + cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: ModelOptions } + +const geminiSettings: ProviderSettings = { + modelOptions: geminiModelOptions, + modelOptionsFallback: (modelName) => { + return null + } +} + + +// ---------------- OPEN SOURCE MODELS ---------------- + +const openSourceModelDefaultOptionsAssumingOAICompat = { + 'deepseekR1': { + supportsFIM: false, + supportsSystemMessage: false, + supportsTools: false, + supportsReasoningOutput: { openSourceThinkTags: ['', ''] }, + }, + 'deepseekCoderV2': { + supportsFIM: false, + supportsSystemMessage: false, // unstable + supportsTools: false, + supportsReasoningOutput: false, + }, + 'codestral': { + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + // llama + 'llama3': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'llama3.1': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'llama3.2': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'llama3.3': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'qwen2.5coder': { + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + // FIM only + 'starcoder2': { + supportsFIM: true, + supportsSystemMessage: false, + supportsTools: false, + supportsReasoningOutput: false, + }, + 'codegemma:2b': { + supportsFIM: true, + supportsSystemMessage: false, + supportsTools: false, + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: Partial } + + + +// ---------------- DEEPSEEK API ---------------- +const deepseekModelOptions = { + 'deepseek-chat': { + ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, + contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing + cost: { cache_read: .07, input: .27, output: 1.10, }, + }, + 'deepseek-reasoner': { + ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, + contextWindow: 64_000, + cost: { cache_read: .14, input: .55, output: 2.19, }, + }, +} as const satisfies { [s: string]: ModelOptions } + + +const deepseekSettings: ProviderSettings = { + modelOptions: deepseekModelOptions, + ifSupportsReasoningOutput: { + // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model + output: { nameOfFieldInDelta: 'reasoning_content' }, + }, + modelOptionsFallback: (modelName) => { + return null + } +} + +// ---------------- GROQ ---------------- +const groqModelOptions = { + 'llama-3.3-70b-versatile': { + contextWindow: 128_000, + cost: { input: 0.59, output: 0.79 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'llama-3.1-8b-instant': { + contextWindow: 128_000, + cost: { input: 0.05, output: 0.08 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'qwen-2.5-coder-32b': { + contextWindow: 128_000, + cost: { input: 0.79, output: 0.79 }, + supportsFIM: false, // unfortunately looks like no FIM support on groq + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: ModelOptions } +const groqSettings: ProviderSettings = { + modelOptions: groqModelOptions, + modelOptionsFallback: (modelName) => { return null } +} + + +// ---------------- anything self-hosted/local: VLLM, OLLAMA, OPENAICOMPAT ---------------- + +// fallback to any model (anything openai-compatible) +const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => { + const toFallback = (opts: Omit): ModelOptions & { modelName: string } => { + return { + modelName, + ...opts, + supportsSystemMessage: opts.supportsSystemMessage ? 'system-role' : false, + cost: { input: 0, output: 0 }, + } + } + if (modelName.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o']) + if (modelName.includes('claude')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022']) + if (modelName.includes('grok')) return toFallback(xAIModelOptions['grok-2-latest']) + if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 32_000, }) + if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 32_000, }) + if (modelName.includes('llama3')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.llama3, contextWindow: 32_000, }) + if (modelName.includes('qwen2.5-coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, }) + if (modelName.includes('codestral')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 32_000, }) + if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) + return toFallback(modelOptionDefaults) +} + + +const vLLMSettings: ProviderSettings = { + // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions + ifSupportsReasoningOutput: { output: { nameOfFieldInDelta: 'reasoning_content' }, }, + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), + modelOptions: {}, +} + +const ollamaSettings: ProviderSettings = { + // reasoning: we need to filter out reasoning tags manually + ifSupportsReasoningOutput: { output: { needsManualParse: true }, }, + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), + modelOptions: {}, +} + +const openaiCompatible: ProviderSettings = { + // reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), + modelOptions: {}, +} + + +// ---------------- OPENROUTER ---------------- +const openRouterModelOptions = { + 'deepseek/deepseek-r1': { + ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, + contextWindow: 128_000, + cost: { input: 0.8, output: 2.4 }, + }, + 'anthropic/claude-3.5-sonnet': { + contextWindow: 200_000, + cost: { input: 3.00, output: 15.00 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'mistralai/codestral-2501': { + ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, + contextWindow: 256_000, + cost: { input: 0.3, output: 0.9 }, + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: ModelOptions } + +const openRouterSettings: ProviderSettings = { + // reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models + ifSupportsReasoningOutput: { + input: { includeInPayload: { include_reasoning: true } }, + output: { nameOfFieldInDelta: 'reasoning' }, + }, + modelOptions: openRouterModelOptions, + // TODO!!! send a query to openrouter to get the price, isFIM, etc. + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), +} + +// ---------------- model settings of everything above ---------------- const modelSettingsOfProvider: ModelSettingsOfProvider = { openAI: openAISettings, anthropic: anthropicSettings, - xAI: XAISettings, - gemini: { - modelOptions: { - - } - }, - googleVertex: { - - }, - microsoftAzure: { - - }, - openRouter: { - providerReasoningOptions: { - // reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models - input: { includeInPayload: { include_reasoning: true } }, - output: { nameOfFieldInDelta: 'reasoning' }, - } - }, - vLLM: { - providerReasoningOptions: { - // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions - output: { nameOfFieldInDelta: 'reasoning_content' }, - } - }, - deepseek: { - providerReasoningOptions: { - // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model - output: { nameOfFieldInDelta: 'reasoning_content' }, - }, - }, - ollama: { - providerReasoningOptions: { - // reasoning: we need to filter out reasoning tags manually - output: { needsManualParse: true }, - }, - }, - - openAICompatible: { - }, - mistral: { - }, - groq: { - }, + xAI: xAISettings, + gemini: geminiSettings, + // open source models + deepseek: deepseekSettings, + groq: groqSettings, + // open source models + providers (mixture of everything) + openRouter: openRouterSettings, + vLLM: vLLMSettings, + ollama: ollamaSettings, + openAICompatible: openaiCompatible, + // googleVertex: {}, + // microsoftAzure: {}, } as const satisfies ModelSettingsOfProvider -const modelOptionsOfProvider = (providerName: ProviderName, modelName: string) => { + + +export const modelOptionsOfProvider = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string } => { const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] - if (modelName in modelOptions) return modelOptions[modelName] - return modelOptionsFallback(modelName) + if (modelName in modelOptions) return { modelName, ...modelOptions[modelName] } + const result = modelOptionsFallback(modelName) + if (!result) return { modelName, ...modelOptionDefaults } + return result } @@ -361,10 +586,6 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } - else if (providerName === 'mistral') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) - } else if (providerName === 'groq') { const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) @@ -379,33 +600,52 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay -const manualParseOnText = ( - providerName: ProviderName, - modelName: string, - onText_: OnText -): OnText => { - return onText_ +const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { + const { modelName, } = modelOptionsOfProvider(providerName, modelName_) + const messages = prepareFIMMessage({ messages: messages_, aiInstructions, }) + + const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) + openai.completions + .create({ + model: modelName, + prompt: messages.prefix, + suffix: messages.suffix, + stop: messages.stopTokens, + max_tokens: messages.maxTokens, + }) + .then(async response => { + const fullText = response.choices[0]?.text + onFinalMessage({ fullText, }); + }) + .catch(error => { + if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }); } + else { onError({ message: error + '', fullError: error }); } + }) } -const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + + +const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { - supportsReasoning: modelSupportsReasoning, + modelName, + supportsReasoningOutput, supportsSystemMessage, supportsTools, - } = modelOptionsOfProvider(providerName, modelName) + } = modelOptionsOfProvider(providerName, modelName_) const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined - const includeInPayload = modelSupportsReasoning ? {} : modelSettingsOfProvider[providerName].providerReasoningOptions?.input?.includeInPayload || {} + const includeInPayload = supportsReasoningOutput ? modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.input?.includeInPayload || {} : {} const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj } - const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].providerReasoningOptions?.output ?? {} - if (needsManualReasoningParse) onText = manualParseOnText(providerName, modelName, onText) + const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.output ?? {} + if (needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags) + onText = extractReasoningFromText(onText, supportsReasoningOutput.openSourceThinkTags) let fullReasoning = '' let fullText = '' @@ -432,7 +672,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage let newReasoning = '' if (nameOfReasoningFieldInDelta) { // @ts-ignore - newReasoning = (chunk.choices[0]?.delta?.[nameOfFieldInDelta] || '') + '' + newReasoning = (chunk.choices[0]?.delta?.[nameOfReasoningFieldInDelta] || '') + '' fullReasoning += newReasoning } @@ -477,10 +717,6 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, -// ------------ OPENAI ------------ -const sendOpenAIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} // ------------ ANTHROPIC ------------ const toAnthropicTool = (toolInfo: InternalToolInfo) => { @@ -504,13 +740,14 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[ }).filter(t => !!t) } -const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { +const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { // supportsReasoning: modelSupportsReasoning, + modelName, supportsSystemMessage, supportsTools, contextWindow, - } = modelOptionsOfProvider(providerName, modelName) + } = modelOptionsOfProvider(providerName, modelName_) const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) @@ -561,16 +798,6 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM // }) -// ------------ XAI ------------ -const sendXAIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ GEMINI ------------ -const sendGeminiAPIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - // ------------ OLLAMA ------------ const newOllamaSDK = ({ endpoint }: { endpoint: string }) => { // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in @@ -603,10 +830,12 @@ const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOf } } -const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { +const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName, aiInstructions, _setAborter }: SendFIMParams_Internal) => { const thisConfig = settingsOfProvider.ollama const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) + const messages = prepareFIMMessage({ messages: messages_, aiInstructions, }) + let fullText = '' ollama.generate({ model: modelName, @@ -614,7 +843,7 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, suffix: messages.suffix, options: { stop: messages.stopTokens, - num_predict: 300, // max tokens + num_predict: messages.maxTokens, // max tokens // repeat_penalty: 1, }, raw: true, @@ -635,57 +864,73 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, } -// ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat! -const sendOllamaChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) + +type CallFnOfProvider = { + [providerName in ProviderName]: { + sendChat: (params: SendChatParams_Internal) => void; + sendFIM: ((params: SendFIMParams_Internal) => void) | null; + list: ((params: ListParams_Internal) => void) | null; + } } -// ------------ OPENAI-COMPATIBLE ------------ -// TODO!!! FIM - -// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ OPENROUTER ------------ -const sendOpenRouterChat = (params: SendChatParams_Internal) => { - _sendOpenAICompatibleChat(params) -} - -// ------------ VLLM ------------ -const vLLMList = async (params: ListParams_Internal) => { - return _openaiCompatibleList(params) -} -const sendVLLMFIM = (params: SendFIMParams_Internal) => { - // TODO!!! -} - -// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -const sendVLLMChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ DEEPSEEK API ------------ -const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ MISTRAL ------------ -const sendMistralAPIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ GROQ ------------ -const sendGroqAPIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} +export const sendLLMMessageToProviderImplementation = { + anthropic: { + sendChat: sendAnthropicChat, + sendFIM: null, + list: null, + }, + openAI: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: null, + list: null, + }, + xAI: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: null, + list: null, + }, + gemini: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: null, + list: null, + }, + ollama: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: sendOllamaFIM, + list: ollamaList, + }, + openAICompatible: { + sendChat: (params) => _sendOpenAICompatibleChat(params), // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration + sendFIM: (params) => _sendOpenAICompatibleFIM(params), + list: null, + }, + openRouter: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: (params) => _sendOpenAICompatibleFIM(params), + list: null, + }, + vLLM: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: (params) => _sendOpenAICompatibleFIM(params), + list: (params) => _openaiCompatibleList(params), + }, + deepseek: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: null, + list: null, + }, + groq: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: null, + list: null, + }, +} satisfies CallFnOfProvider /* -FIM: +FIM info (this may be useful in the future with vLLM, but in most cases the only way to use FIM is if the provider explicitly supports it): qwen2.5-coder https://ollama.com/library/qwen2.5-coder/blobs/e94a8ecb9327 <|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> @@ -706,71 +951,3 @@ codegemma https://ollama.com/library/codegemma:2b/blobs/48d9a8140749 <|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> */ - - - -type CallFnOfProvider = { - [providerName in ProviderName]: { - sendChat: (params: SendChatParams_Internal) => void; - sendFIM: ((params: SendFIMParams_Internal) => void) | null; - list: ((params: ListParams_Internal) => void) | null; - } -} -export const sendLLMMessageToProviderImplementation = { - openAI: { - sendChat: sendOpenAIChat, - sendFIM: null, - list: null, - }, - anthropic: { - sendChat: sendAnthropicChat, - sendFIM: null, - list: null, - }, - xAI: { - sendChat: sendXAIChat, - sendFIM: null, - list: null, - }, - gemini: { - sendChat: sendGeminiAPIChat, - sendFIM: null, - list: null, - }, - ollama: { - sendChat: sendOllamaChat, - sendFIM: sendOllamaFIM, - list: ollamaList, - }, - openAICompatible: { - sendChat: sendOpenAICompatibleChat, - sendFIM: null, - list: null, - }, - openRouter: { - sendChat: sendOpenRouterChat, - sendFIM: null, - list: null, - }, - vLLM: { - sendChat: sendVLLMChat, - sendFIM: sendVLLMFIM, - list: vLLMList, - }, - deepseek: { - sendChat: sendDeepSeekAPIChat, - sendFIM: null, - list: null, - }, - groq: { - sendChat: sendGroqAPIChat, - sendFIM: null, - list: null, - }, - mistral: { - sendChat: sendMistralAPIChat, - sendFIM: null, - list: null, - }, - -} satisfies CallFnOfProvider diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 1d388338..1aec2649 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -1,6 +1,6 @@ -import { LLMChatMessage } from '../../common/llmMessageTypes.js'; +import { LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js'; import { deepClone } from '../../../../../base/common/objects.js'; @@ -322,3 +322,27 @@ export const prepareMessages = ({ } as const } + + + + +export const prepareFIMMessage = ({ + messages, + aiInstructions, +}: { + messages: LLMFIMMessage, + aiInstructions: string, +}) => { + + let prefix = `\ +## You are a helpful coding assistant that performs autocomplete (fill-in-the middle or "FIM") for the user. +${!aiInstructions ? '' : `\ +## Special user instructions: +${aiInstructions.split('\n').map(line => `##${line}`).join('\n')}`} + +${messages.prefix}` + + const suffix = messages.suffix + const stopTokens = messages.stopTokens + return { prefix, suffix, stopTokens, maxTokens: 300 } as const +} From d2fb0fb4fff7aca0c7c0bc57d84fefe9a43ee1be Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 04:32:53 -0800 Subject: [PATCH 89/92] maxTokens for anthropic --- .../void/electron-main/llmMessage/MODELS.ts | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index d68408cd..14f5ec44 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -17,7 +17,8 @@ import { extractReasoningFromText } from '../../browser/helpers/extractCodeFromR type ModelOptions = { - contextWindow: number; + contextWindow: number; // input tokens + maxOutputTokens: number | null; // output tokens cost: { input: number; output: number; @@ -70,6 +71,7 @@ type ModelSettingsOfProvider = { const modelOptionDefaults: ModelOptions = { contextWindow: 32_000, + maxOutputTokens: null, cost: { input: 0, output: 0 }, supportsSystemMessage: false, supportsTools: false, @@ -82,6 +84,7 @@ const modelOptionDefaults: ModelOptions = { const openAIModelOptions = { // https://platform.openai.com/docs/pricing 'o1': { contextWindow: 128_000, + maxOutputTokens: 100_000, cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, supportsFIM: false, supportsTools: false, @@ -90,6 +93,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing }, 'o3-mini': { contextWindow: 200_000, + maxOutputTokens: 100_000, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, supportsFIM: false, supportsTools: false, @@ -98,6 +102,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing }, 'gpt-4o': { contextWindow: 128_000, + maxOutputTokens: 16_384, cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, supportsFIM: false, supportsTools: 'openai-style', @@ -106,6 +111,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing }, 'o1-mini': { contextWindow: 128_000, + maxOutputTokens: 65_536, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, supportsFIM: false, supportsTools: false, @@ -114,6 +120,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing }, 'gpt-4o-mini': { contextWindow: 128_000, + maxOutputTokens: 16_384, cost: { input: 0.15, cache_read: 0.075, output: 0.60, }, supportsFIM: false, supportsTools: 'openai-style', @@ -139,6 +146,7 @@ const openAISettings: ProviderSettings = { const anthropicModelOptions = { 'claude-3-5-sonnet-20241022': { contextWindow: 200_000, + maxOutputTokens: 8_192, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, supportsFIM: false, supportsSystemMessage: 'separated', @@ -147,6 +155,7 @@ const anthropicModelOptions = { }, 'claude-3-5-haiku-20241022': { contextWindow: 200_000, + maxOutputTokens: 8_192, cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, supportsFIM: false, supportsSystemMessage: 'separated', @@ -155,6 +164,7 @@ const anthropicModelOptions = { }, 'claude-3-opus-20240229': { contextWindow: 200_000, + maxOutputTokens: 4_096, cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, supportsFIM: false, supportsSystemMessage: 'separated', @@ -163,6 +173,7 @@ const anthropicModelOptions = { }, 'claude-3-sonnet-20240229': { // no point of using this, but including this for people who put it in contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + maxOutputTokens: 4_096, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', @@ -177,8 +188,9 @@ const anthropicSettings: ProviderSettings = { if (modelName.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022' if (modelName.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022' if (modelName.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229' + if (modelName.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229' if (fallbackName) return { modelName: fallbackName, ...anthropicModelOptions[fallbackName] } - return null + return { modelName, ...modelOptionDefaults, maxOutputTokens: 4_096 } } } @@ -187,6 +199,7 @@ const anthropicSettings: ProviderSettings = { const xAIModelOptions = { 'grok-2-latest': { contextWindow: 131_072, + maxOutputTokens: null, // 131_072, cost: { input: 2.00, output: 10.00 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -210,6 +223,7 @@ const xAISettings: ProviderSettings = { const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing 'gemini-2.0-flash': { contextWindow: 1_048_576, + maxOutputTokens: null, // 8_192, cost: { input: 0.10, output: 0.40 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -218,6 +232,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-2.0-flash-lite-preview-02-05': { contextWindow: 1_048_576, + maxOutputTokens: null, // 8_192, cost: { input: 0.075, output: 0.30 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -226,6 +241,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-1.5-flash': { contextWindow: 1_048_576, + maxOutputTokens: null, // 8_192, cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now supportsFIM: false, supportsSystemMessage: 'system-role', @@ -234,6 +250,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-1.5-pro': { contextWindow: 2_097_152, + maxOutputTokens: null, // 8_192, cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now supportsFIM: false, supportsSystemMessage: 'system-role', @@ -242,6 +259,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-1.5-flash-8b': { contextWindow: 1_048_576, + maxOutputTokens: null, // 8_192, cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now supportsFIM: false, supportsSystemMessage: 'system-role', @@ -332,11 +350,13 @@ const deepseekModelOptions = { 'deepseek-chat': { ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing + maxOutputTokens: null, // 8_000, cost: { cache_read: .07, input: .27, output: 1.10, }, }, 'deepseek-reasoner': { ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 64_000, + maxOutputTokens: null, // 8_000, cost: { cache_read: .14, input: .55, output: 2.19, }, }, } as const satisfies { [s: string]: ModelOptions } @@ -357,6 +377,7 @@ const deepseekSettings: ProviderSettings = { const groqModelOptions = { 'llama-3.3-70b-versatile': { contextWindow: 128_000, + maxOutputTokens: null, // 32_768, cost: { input: 0.59, output: 0.79 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -365,6 +386,7 @@ const groqModelOptions = { }, 'llama-3.1-8b-instant': { contextWindow: 128_000, + maxOutputTokens: null, // 8_192, cost: { input: 0.05, output: 0.08 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -373,6 +395,7 @@ const groqModelOptions = { }, 'qwen-2.5-coder-32b': { contextWindow: 128_000, + maxOutputTokens: null, // not specified? cost: { input: 0.79, output: 0.79 }, supportsFIM: false, // unfortunately looks like no FIM support on groq supportsSystemMessage: 'system-role', @@ -401,11 +424,11 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (modelName.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o']) if (modelName.includes('claude')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022']) if (modelName.includes('grok')) return toFallback(xAIModelOptions['grok-2-latest']) - if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 32_000, }) - if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 32_000, }) - if (modelName.includes('llama3')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.llama3, contextWindow: 32_000, }) - if (modelName.includes('qwen2.5-coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, }) - if (modelName.includes('codestral')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 32_000, }) + if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('llama3')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('qwen2.5-coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('codestral')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionDefaults) } @@ -437,10 +460,12 @@ const openRouterModelOptions = { 'deepseek/deepseek-r1': { ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 128_000, + maxOutputTokens: null, cost: { input: 0.8, output: 2.4 }, }, 'anthropic/claude-3.5-sonnet': { contextWindow: 200_000, + maxOutputTokens: null, cost: { input: 3.00, output: 15.00 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -450,6 +475,7 @@ const openRouterModelOptions = { 'mistralai/codestral-2501': { ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 256_000, + maxOutputTokens: null, cost: { input: 0.3, output: 0.9 }, supportsTools: 'openai-style', supportsReasoningOutput: false, From 5cbd0a19a4f1696393865258ad7d7872ff7f394a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 04:38:08 -0800 Subject: [PATCH 90/92] minor fixes --- .../void/browser/helpers/extractCodeFromResult.ts | 2 +- .../contrib/void/electron-main/llmMessage/MODELS.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 297d82b6..00eb2ef1 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -250,7 +250,7 @@ export const extractSearchReplaceBlocks = (str: string) => { - +// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true export const extractReasoningFromText = ( onText_: OnText, thinkTags: [string, string], diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index 14f5ec44..6d39ee9f 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -627,7 +627,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { - const { modelName, } = modelOptionsOfProvider(providerName, modelName_) + const { modelName } = modelOptionsOfProvider(providerName, modelName_) const messages = prepareFIMMessage({ messages: messages_, aiInstructions, }) const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) @@ -658,6 +658,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage supportsReasoningOutput, supportsSystemMessage, supportsTools, + maxOutputTokens, } = modelOptionsOfProvider(providerName, modelName_) const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) @@ -666,8 +667,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const includeInPayload = supportsReasoningOutput ? modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.input?.includeInPayload || {} : {} const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} + const maxTokensObj = maxOutputTokens ? { max_tokens: maxOutputTokens } : {} const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) - const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj } + const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, ...maxTokensObj } const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.output ?? {} if (needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags) @@ -772,7 +774,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM modelName, supportsSystemMessage, supportsTools, - contextWindow, + maxOutputTokens, } = modelOptionsOfProvider(providerName, modelName_) const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) @@ -785,7 +787,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM system: separateSystemMessageStr, messages: messages, model: modelName, - max_tokens: contextWindow, + max_tokens: maxOutputTokens ?? 4_096, // anthropic requires this tools: tools, tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time }) From 5c047b78f36d09227550b7325c8fc6bbc103731d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 05:33:45 -0800 Subject: [PATCH 91/92] style+FIM prompt --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/electron-main/llmMessage/MODELS.ts | 10 ++++++---- .../electron-main/llmMessage/preprocessLLMMessages.ts | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) 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 96e22bd4..979ae67b 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 @@ -1020,7 +1020,7 @@ export const SidebarChat = () => { {/* error message */} {latestError === undefined ? null : -
    +
    `Invalid ${displayInfoOfProviderName(providerName).title} API key.` + // ---------------- OPENAI ---------------- const openAIModelOptions = { // https://platform.openai.com/docs/pricing @@ -644,7 +646,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError onFinalMessage({ fullText, }); }) .catch(error => { - if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }); } + if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); } else { onError({ message: error + '', fullError: error }); } }) } @@ -710,7 +712,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage }) // when error/fail - this catches errors of both .create() and .then(for await) .catch(error => { - if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }); } + if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); } else { onError({ message: error + '', fullError: error }); } }) } @@ -803,7 +805,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM }) // on error stream.on('error', (error) => { - if (error instanceof Anthropic.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }) } + if (error instanceof Anthropic.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }) } else { onError({ message: error + '', fullError: error }) } }) _setAborter(() => stream.controller.abort()) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 1aec2649..40eb880c 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -335,9 +335,9 @@ export const prepareFIMMessage = ({ }) => { let prefix = `\ -## You are a helpful coding assistant that performs autocomplete (fill-in-the middle or "FIM") for the user. +## You are a helpful coding assistant that performs autocomplete. ${!aiInstructions ? '' : `\ -## Special user instructions: +## Instructions: ${aiInstructions.split('\n').map(line => `##${line}`).join('\n')}`} ${messages.prefix}` From 4a43b628d472849dddeade3d706b451d1bf96fd1 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 06:23:24 -0800 Subject: [PATCH 92/92] improvements --- .../react/src/void-settings-tsx/Settings.tsx | 35 +++++++------ .../contrib/void/common/toolsService.ts | 4 +- .../void/common/voidSettingsService.ts | 52 ++++++++++--------- .../contrib/void/common/voidSettingsTypes.ts | 2 +- .../void/electron-main/llmMessage/MODELS.ts | 27 ++++++++-- .../llmMessage/preprocessLLMMessages.ts | 10 ++-- 6 files changed, 77 insertions(+), 53 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index e2056fbf..7f28467f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -5,7 +5,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' -import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../common/voidSettingsTypes.js' +import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName } from '../../../../common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' @@ -368,15 +368,15 @@ export const AutoRefreshToggle = () => { // right now this is just `enabled_autoRefreshModels` const enabled = voidSettingsState.globalSettings[settingName] - return { - voidSettingsService.setGlobalSetting(settingName, !enabled) - metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) - }} - text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} - icon={enabled ? : } - disabled={false} - /> + return { + voidSettingsService.setGlobalSetting(settingName, !enabled) + metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) + }} + text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} + icon={enabled ? : } + disabled={false} + /> } @@ -401,7 +401,7 @@ export const FeaturesTab = () => { -
    +
    @@ -437,12 +437,13 @@ export const FeaturesTab = () => {

    Feature Options

    {featureNames.map(featureName => -
    -

    {displayInfoOfFeatureName(featureName)}

    - -
    + (['Ctrl+L', 'Ctrl+K'] as FeatureName[]).includes(featureName) ? null : +
    +

    {displayInfoOfFeatureName(featureName)}

    + +
    )}
    diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 4e92696c..f27739c0 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -55,7 +55,7 @@ export const voidTools = { query: { type: 'string', description: undefined }, ...paginationHelper.param, }, - required: ['query'] + required: ['query'], }, search: { @@ -305,6 +305,8 @@ export class ToolsService implements IToolsService { return { queryStr, uris, hasNextPage } }, search: async (s: string) => { + + console.log('search') const o = validateJSON(s) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index be3f6689..72095c83 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -63,7 +63,30 @@ export interface IVoidSettingsService { -const _updatedValidatedState = (state: Omit) => { + +const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { + const { existingModels } = options + + const existingModelsMap: Record = {} + for (const existingModel of existingModels) { + existingModelsMap[existingModel.modelName] = existingModel + } + + const newDefaultModels = defaultModelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: true, + isHidden: !!existingModelsMap[modelName]?.isHidden, + })) + + return [ + ...newDefaultModels, // swap out all the default models for the new default models + ...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models + ] +} + + +const _validatedState = (state: Omit) => { let newSettingsOfProvider = state.settingsOfProvider @@ -201,7 +224,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { modelSelectionOfFeature: newModelSelectionOfFeature, } - this.state = _updatedValidatedState(readS) + this.state = _validatedState(readS) resolver() this._onDidChangeState.fire() @@ -248,7 +271,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { globalSettings: newGlobalSettings, } - this.state = _updatedValidatedState(newState) + this.state = _validatedState(newState) await this._storeState() this._onDidChangeState.fire() @@ -290,27 +313,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } - private _updatedModelsAfterAutodetection = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { - const { existingModels } = options - - const existingModelsMap: Record = {} - for (const existingModel of existingModels) { - existingModelsMap[existingModel.modelName] = existingModel - } - - const newDefaultModels = defaultModelNames.map((modelName, i) => ({ - modelName, - isDefault: true, - isAutodetected: true, - isHidden: !!existingModelsMap[modelName]?.isHidden, - })) - - return [ - ...newDefaultModels, // swap out all the default models for the new default models - ...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models - ] - } - setAutodetectedModels(providerName: ProviderName, autodetectedModelNames: string[], logging: object) { @@ -318,7 +320,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const { models } = this.state.settingsOfProvider[providerName] const oldModelNames = models.map(m => m.modelName) - const newModels = this._updatedModelsAfterAutodetection(autodetectedModelNames, { existingModels: models }) + const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models }) this.setSettingOfProvider(providerName, 'models', newModels) // if the models changed, log it diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 4111b53b..379a4817 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -83,7 +83,7 @@ export const defaultModelsOfProvider = { 'anthropic/claude-3.5-sonnet', 'deepseek/deepseek-r1', 'mistralai/codestral-2501', - 'qwen/qwen2.5-vl-72b-instruct:free', + 'qwen/qwen-2.5-coder-32b-instruct', ], groq: [ // https://console.groq.com/docs/models 'llama-3.3-70b-versatile', diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index 872d5058..a4ad5487 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -429,7 +429,7 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('llama3')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('qwen2.5-coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('codestral')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionDefaults) @@ -482,6 +482,15 @@ const openRouterModelOptions = { supportsTools: 'openai-style', supportsReasoningOutput: false, }, + 'qwen/qwen-2.5-coder-32b-instruct': { + ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], + contextWindow: 33_000, + maxOutputTokens: null, + supportsTools: false, // openrouter qwen doesn't seem to support tools...? + cost: { input: 0.07, output: 0.16 }, + } + + } as const satisfies { [s: string]: ModelOptions } const openRouterSettings: ProviderSettings = { @@ -520,7 +529,7 @@ const modelSettingsOfProvider: ModelSettingsOfProvider = { -export const modelOptionsOfProvider = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string } => { +export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string } => { const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] if (modelName in modelOptions) return { modelName, ...modelOptions[modelName] } const result = modelOptionsFallback(modelName) @@ -629,7 +638,15 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { - const { modelName } = modelOptionsOfProvider(providerName, modelName_) + const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) + if (!supportsFIM) { + if (modelName === modelName_) + onFinalMessage({ fullText: `Model ${modelName} does not support FIM.` }) + else + onFinalMessage({ fullText: `Model ${modelName_} (${modelName}) does not support FIM.` }) + return + } + const messages = prepareFIMMessage({ messages: messages_, aiInstructions, }) const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) @@ -661,7 +678,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage supportsSystemMessage, supportsTools, maxOutputTokens, - } = modelOptionsOfProvider(providerName, modelName_) + } = getModelCapabilities(providerName, modelName_) const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined @@ -777,7 +794,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM supportsSystemMessage, supportsTools, maxOutputTokens, - } = modelOptionsOfProvider(providerName, modelName_) + } = getModelCapabilities(providerName, modelName_) const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 40eb880c..32b91d07 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -335,14 +335,16 @@ export const prepareFIMMessage = ({ }) => { let prefix = `\ -## You are a helpful coding assistant that performs autocomplete. ${!aiInstructions ? '' : `\ -## Instructions: -${aiInstructions.split('\n').map(line => `##${line}`).join('\n')}`} +// Instructions: +// Do not output an explanation. Try to avoid outputting comments. Only output the middle code. +${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`} ${messages.prefix}` const suffix = messages.suffix const stopTokens = messages.stopTokens - return { prefix, suffix, stopTokens, maxTokens: 300 } as const + const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const + console.log('ret', ret) + return ret }