Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c642763f0 | |||
| 5815c0180a | |||
| 822430535e | |||
| 020fb60d74 | |||
| 615f8c3315 | |||
| 980d7937c3 | |||
| 57d94d2d44 | |||
| 382ecaae1b | |||
| a9abdf6351 | |||
| 27c40d8428 | |||
| c067d9fbf0 | |||
| 83c4a05642 | |||
| 182af95035 | |||
| 0a6be740e1 | |||
| 8d0cdb8d36 | |||
| 29f004a826 | |||
| ac6b5601ef | |||
| cfb15e1117 | |||
| c12e9ca9f8 | |||
| e138744816 | |||
| df21a99ed9 | |||
| 98e07e1c7f | |||
| 77e562e278 | |||
| 232264d5fe | |||
| 70d484049c | |||
| f4557ef6a8 | |||
| 3038b420c9 | |||
| 0625ce4b40 | |||
| 44f56b3ab4 | |||
| 826571b410 | |||
| c185807e60 | |||
| 31cde552a0 | |||
| 23bb28f3c0 | |||
| 5a34feb328 | |||
| f0f536bc2d | |||
| 09f5f1fad1 | |||
| 31aa31a18b | |||
| b56cc0d662 | |||
| 4f4af9b8b9 | |||
| 66de84f4e3 | |||
| 0a02342128 | |||
| f99589aaa6 | |||
| da6ffecc1d | |||
| 43b6dba7a3 | |||
| 5747904b85 | |||
| 4358b9e384 | |||
| 5a5732601e | |||
| 83ec200039 | |||
| 81ab21868a | |||
| 627ec18cc9 | |||
| 883a4663c9 | |||
| d2d47175b5 | |||
| 7aacbca580 | |||
| f5b0f24445 | |||
| 89ac548ae9 | |||
| 57bc32ac9b | |||
| 446248aa79 | |||
| 66575b5381 | |||
| 8b99b6ec78 | |||
| 18d1f2e6b9 | |||
| 72143608a1 | |||
| ec2ea70395 | |||
| 55691624dd | |||
| 9072668be3 | |||
| 7a4cf6c955 | |||
| 588953be96 | |||
| 5bdbbc84aa | |||
| ebae55cd4b | |||
| 2b9e2088b3 | |||
| cbc7cf0841 | |||
| d141bffae9 | |||
| 3619a3e671 | |||
| 13654389c6 | |||
| 4d8363026e | |||
| d80fd766f8 | |||
| 6d9027f63b | |||
| 03ab6559fc | |||
| 35f23261b0 | |||
| 9b4c0b8d5a | |||
| 07df20b4de | |||
| 5995591296 | |||
| 6646bd0993 | |||
| f1c9e29f61 | |||
| 2b97326eff | |||
| b70ee20b88 | |||
| ae0068ecb3 | |||
| 091bbf4dd3 | |||
| 7642b3281a | |||
| 05e27a0885 | |||
| 1d15d29a19 | |||
| 807086f7b6 | |||
| 2d910835b7 | |||
| b68d8fd8e8 | |||
| 59dd7d7eea | |||
| 079ce49d17 | |||
| 7f4470674d | |||
| b1b7afe8a0 | |||
| d3757e81b4 | |||
| 065470afc1 | |||
| db771c4f33 | |||
| 3c96193b71 | |||
| c669a987c5 | |||
| 543c6f0550 | |||
| 8afaec30da | |||
| a0f9309e02 | |||
| 243349a90f | |||
| 04a821b382 | |||
| 906502f660 | |||
| 2817cdd8a6 | |||
| a8bc42b524 | |||
| 05e1e61f08 | |||
| 30655678fe | |||
| ca51bcaecc | |||
| a629736683 | |||
| 0074189191 | |||
| 4a862641cd | |||
| b18ac4b6c0 | |||
| d7201a38dc | |||
| 9112c0c94d | |||
| 716cb8d591 | |||
| aed9f31185 | |||
| 2377c18241 | |||
| 7b9e5ce517 | |||
| c119286571 | |||
| 5cab7e61e5 | |||
| c405a462a8 | |||
| e481327327 | |||
| b7bb7f2f94 | |||
| 2ae288e2ed | |||
| 316709671f | |||
| 9fac1e27a9 | |||
| f632327a7e | |||
| e95922802f | |||
| 3954056a41 | |||
| 1c7acdac94 | |||
| 33163b7e81 | |||
| d9d3147b2a | |||
| 4b9e2c2319 | |||
| 19bd649451 | |||
| 7fd06b946b | |||
| 8c760980b6 | |||
| 96000273cf | |||
| e771edbe2e | |||
| aafe556333 | |||
| 9502f563f4 | |||
| 78c6baed82 | |||
| bafbdd9df3 | |||
| 4cca689c94 | |||
| 63083cec89 | |||
| 661908237b | |||
| b35dfdf475 | |||
| 2187641cc3 | |||
| 609f4c9cbf | |||
| 6df3b588df | |||
| 01329382b9 | |||
| 7af9973a8d | |||
| 1ed79ef985 | |||
| 8a12505ffd | |||
| f82c15401a | |||
| b4dff124e0 | |||
| 1aeb62d075 | |||
| 4b86bfa184 | |||
| f4c1321f25 | |||
| b54dd7fedc | |||
| 7c448f15db | |||
| 3241178238 | |||
| 68bb3f6962 | |||
| 2e64f5a24b | |||
| 2e5ecb291d | |||
| f1e24c9b4c | |||
| f65e4f51a2 | |||
| f1a5d20150 | |||
| 28dcdff189 | |||
| 67ed52415e | |||
| 3772cf98ea | |||
| 1c6edf7023 | |||
| 6b6528c3fb | |||
| f98894ad77 | |||
| f8745d8b40 | |||
| 7066b4285b | |||
| 1eee07e697 | |||
| 45f7e4b157 | |||
| 1619a3c19b | |||
| d668348244 | |||
| 3fa680e8ba | |||
| e8b7c9c470 | |||
| a0eb7d6192 | |||
| 5c4785d691 | |||
| 1de90cc39d | |||
| d09a41acd3 | |||
| 24102fc2b2 | |||
| c2b65eb16f | |||
| a4046bc5a5 | |||
| 6f73804e94 | |||
| 76155beaad | |||
| 53c68b1267 | |||
| afbc1b40bf | |||
| 9a905f6cb2 | |||
| dd42e40a8d | |||
| 970fd7ecbb | |||
| 50710143eb | |||
| d6295e5d65 | |||
| 053a022ef8 | |||
| 3b963d8717 | |||
| e8540e4668 | |||
| 39f2f24b42 | |||
| e852874be7 | |||
| 6d07f9db14 | |||
| 2934f9a3c5 | |||
| 7748dc4f2d | |||
| 8b45aea8a8 | |||
| cab8dd64b6 | |||
| 19782c80df | |||
| 926db45f0a | |||
| 4068911d49 | |||
| dcee2e2657 | |||
| 4c1c0b591a | |||
| 61fc593903 | |||
| ecff59eed7 | |||
| e2a7b0e1a0 | |||
| 07c3f33f1e | |||
| 0f93e1b055 | |||
| 2106573922 | |||
| 93a13a76af | |||
| 7b18e25288 | |||
| 57b0505a7e | |||
| 58d95035ff | |||
| 5232b4c30c | |||
| 08274637e1 | |||
| 168b92f6c4 | |||
| a9aa9a26ea |
@@ -8,3 +8,7 @@ Look for services and built-in functions that you might need to use to solve the
|
||||
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
|
||||
|
||||
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
|
||||
|
||||
Never modify files outside src/vs/workbench/contrib/void without consulting with the user first.
|
||||
|
||||
All types that map from a value A to B should be called bOfA. For example, if you create a hashmap that goes from toolId to toolName, it should be called toolNameOfToolId, etc.
|
||||
|
||||
@@ -86,7 +86,7 @@ To build Void from the terminal instead of from inside VSCode, follow the steps
|
||||
|
||||
- Make sure you followed the prerequisite steps above.
|
||||
- Make sure you have Node version `20.18.2` (the version in `.nvmrc`)!
|
||||
- You can do this easily without touching your base installation with [nvm](https://github.com/nvm-sh/nvm). Simply run `nvm install`, followed by `nvm use` and it will automatically install and use the version specified in `nvmrc`
|
||||
- You can do this easily without touching your base installation with [nvm](https://github.com/nvm-sh/nvm). Simply run `nvm install`, followed by `nvm use` and it will automatically install and use the version specified in `nvmrc`.
|
||||
- Make sure that the path to your Void folder does not have any spaces in it.
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
|
||||
- If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`.
|
||||
|
||||
+3
-1
@@ -842,7 +842,9 @@ export default tseslint.config(
|
||||
'@xterm/xterm',
|
||||
'yauzl',
|
||||
'yazl',
|
||||
'zlib'
|
||||
'zlib',
|
||||
// Void added this
|
||||
'@modelcontextprotocol/sdk/**'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Generated
+68
-23
@@ -13,11 +13,11 @@
|
||||
"@anthropic-ai/sdk": "^0.40.0",
|
||||
"@c4312/eventsource-umd": "^3.0.5",
|
||||
"@floating-ui/react": "^0.27.8",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@mistralai/mistralai": "^1.6.0",
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
@@ -299,13 +299,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@azure/core-auth": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz",
|
||||
"integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
|
||||
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-util": "^1.1.0",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -448,18 +449,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-rest-pipeline": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.0.tgz",
|
||||
"integrity": "sha512-CeuTvsXxCUmEuxH5g/aceuSl6w2EugvNHKAtKKVdiX915EjJJxAwfzNNWZreNnbxHZ2fi0zaM6wwS23x2JVqSQ==",
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz",
|
||||
"integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.4.0",
|
||||
"@azure/core-auth": "^1.8.0",
|
||||
"@azure/core-tracing": "^1.0.1",
|
||||
"@azure/core-util": "^1.9.0",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"@typespec/ts-http-runtime": "^0.2.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -479,12 +480,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-util": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz",
|
||||
"integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==",
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.12.0.tgz",
|
||||
"integrity": "sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@typespec/ts-http-runtime": "^0.2.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1817,11 +1820,17 @@
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@google/generative-ai": {
|
||||
"version": "0.24.1",
|
||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
|
||||
"integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
|
||||
"node_modules/@google/genai": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz",
|
||||
"integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^9.14.2",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
@@ -2607,9 +2616,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz",
|
||||
"integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==",
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz",
|
||||
"integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
@@ -4251,6 +4260,21 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typespec/ts-http-runtime": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz",
|
||||
"integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/deviceid": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/deviceid/-/deviceid-0.1.1.tgz",
|
||||
@@ -23767,6 +23791,27 @@
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
|
||||
+2
-2
@@ -75,11 +75,11 @@
|
||||
"@anthropic-ai/sdk": "^0.40.0",
|
||||
"@c4312/eventsource-umd": "^3.0.5",
|
||||
"@floating-ui/react": "^0.27.8",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@mistralai/mistralai": "^1.6.0",
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.3.2",
|
||||
"voidVersion": "1.3.10",
|
||||
"voidRelease": "0034",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
|
||||
@@ -130,6 +130,7 @@ import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpda
|
||||
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
|
||||
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
|
||||
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
|
||||
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
|
||||
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
@@ -1243,6 +1244,9 @@ export class CodeApplication extends Disposable {
|
||||
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
|
||||
|
||||
const mcpChannel = new MCPChannel();
|
||||
mainProcessElectronServer.registerChannel('void-channel-mcp', mcpChannel);
|
||||
|
||||
// Extension Host Debug Broadcasting
|
||||
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
||||
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
|
||||
|
||||
@@ -11,12 +11,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
|
||||
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { chat_userMessageContent, isABuiltinToolName } from '../common/prompt/prompts.js';
|
||||
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js';
|
||||
import { IToolsService } from './toolsService.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
@@ -37,6 +37,8 @@ import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IMCPService } from '../common/mcpService.js';
|
||||
import { RawMCPToolCall } from '../common/mcpServiceTypes.js';
|
||||
|
||||
|
||||
// related to retrying when LLM message has error
|
||||
@@ -181,10 +183,11 @@ export type ThreadStreamState = {
|
||||
llmInfo?: undefined;
|
||||
toolInfo: {
|
||||
toolName: ToolName;
|
||||
toolParams: ToolCallParams[ToolName];
|
||||
toolParams: ToolCallParams<ToolName>;
|
||||
id: string;
|
||||
content: string;
|
||||
rawParams: RawToolParamsObj;
|
||||
mcpServerName: string | undefined;
|
||||
};
|
||||
interrupt: Promise<() => void>;
|
||||
} | {
|
||||
@@ -323,6 +326,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IMCPService private readonly _mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
@@ -445,7 +449,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
// if running now but stream state doesn't indicate it (happens if restart Void), cancel that last tool
|
||||
if (lastMessage && lastMessage.role === 'tool' && lastMessage.type === 'running_now') {
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params })
|
||||
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params, mcpServerName: lastMessage.mcpServerName })
|
||||
}
|
||||
|
||||
}
|
||||
@@ -532,19 +537,23 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||
|
||||
let params: ToolCallParams[ToolName]
|
||||
let params: ToolCallParams<ToolName>
|
||||
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
||||
params = lastMsg.params
|
||||
}
|
||||
else return
|
||||
|
||||
const { name, id, rawParams } = lastMsg
|
||||
const { name, id, rawParams, mcpServerName } = lastMsg
|
||||
|
||||
const errorMessage = this.errMsgs.rejected
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
|
||||
const errorMessage = this.toolErrMsgs.rejected
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams, mcpServerName })
|
||||
this._setStreamState(threadId, undefined)
|
||||
}
|
||||
|
||||
private _computeMCPServerOfToolName = (toolName: string) => {
|
||||
return this._mcpService.getMCPTools()?.find(t => t.name === toolName)?.mcpServerName
|
||||
}
|
||||
|
||||
async abortRunning(threadId: string) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
@@ -553,12 +562,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
if (this.streamState[threadId]?.isRunning === 'LLM') {
|
||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) })
|
||||
}
|
||||
// add tool that's running
|
||||
else if (this.streamState[threadId]?.isRunning === 'tool') {
|
||||
const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo
|
||||
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null })
|
||||
const { toolName, toolParams, id, content: content_, rawParams, mcpServerName } = this.streamState[threadId].toolInfo
|
||||
const content = content_ || this.toolErrMsgs.interrupted
|
||||
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null, mcpServerName })
|
||||
}
|
||||
// reject the tool for the user if relevant
|
||||
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
|
||||
@@ -581,8 +591,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
|
||||
|
||||
private readonly errMsgs = {
|
||||
private readonly toolErrMsgs = {
|
||||
rejected: 'Tool call was rejected by the user.',
|
||||
interrupted: 'Tool call was interrupted by the user.',
|
||||
errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
|
||||
}
|
||||
|
||||
@@ -595,36 +606,46 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
threadId: string,
|
||||
toolName: ToolName,
|
||||
toolId: string,
|
||||
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||
mcpServerName: string | undefined,
|
||||
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams<ToolName> } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
||||
|
||||
// compute these below
|
||||
let toolParams: ToolCallParams[ToolName]
|
||||
let toolResult: Awaited<ToolResultType[typeof toolName]>
|
||||
let toolParams: ToolCallParams<ToolName>
|
||||
let toolResult: ToolResult<ToolName>
|
||||
let toolResultStr: string
|
||||
|
||||
// Check if it's a built-in tool
|
||||
const isBuiltInTool = isABuiltinToolName(toolName)
|
||||
|
||||
|
||||
if (!opts.preapproved) { // skip this if pre-approved
|
||||
// 1. validate tool params
|
||||
try {
|
||||
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
if (isBuiltInTool) {
|
||||
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||
toolParams = params
|
||||
}
|
||||
else {
|
||||
toolParams = opts.unvalidatedToolParams
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, mcpServerName })
|
||||
return {}
|
||||
}
|
||||
// once validated, add checkpoint for edit
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
|
||||
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) }
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['edit_file']).uri }) }
|
||||
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['rewrite_file']).uri }) }
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
|
||||
|
||||
const approvalType = approvalTypeOfToolName[toolName]
|
||||
const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools'
|
||||
if (approvalType) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType]
|
||||
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
@@ -636,9 +657,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 3. call the tool
|
||||
// this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
|
||||
const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams } as const
|
||||
const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName } as const
|
||||
this._updateLatestTool(threadId, runningTool)
|
||||
|
||||
|
||||
@@ -648,13 +672,28 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
try {
|
||||
|
||||
// set stream state
|
||||
this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams } })
|
||||
this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams, mcpServerName } })
|
||||
|
||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||
const interruptor = () => { interrupted = true; interruptTool?.() }
|
||||
resolveInterruptor(interruptor)
|
||||
if (isBuiltInTool) {
|
||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||
const interruptor = () => { interrupted = true; interruptTool?.() }
|
||||
resolveInterruptor(interruptor)
|
||||
|
||||
toolResult = await result
|
||||
toolResult = await result
|
||||
}
|
||||
else {
|
||||
const mcpTools = this._mcpService.getMCPTools()
|
||||
const mcpTool = mcpTools?.find(t => t.name === toolName)
|
||||
if (!mcpTool) { throw new Error(`MCP tool ${toolName} not found`) }
|
||||
|
||||
resolveInterruptor(() => { })
|
||||
|
||||
toolResult = (await this._mcpService.callMCPTool({
|
||||
serverName: mcpTool.mcpServerName ?? 'unknown_mcp_server',
|
||||
toolName: toolName,
|
||||
params: toolParams
|
||||
})).result
|
||||
}
|
||||
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
}
|
||||
@@ -663,21 +702,27 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 4. stringify the result to give to the LLM
|
||||
try {
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
if (isBuiltInTool) {
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
}
|
||||
// For MCP tools, handle the result based on its type
|
||||
else {
|
||||
toolResultStr = this._mcpService.stringifyResult(toolResult as RawMCPToolCall)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = this.errMsgs.errWhenStringifying(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
const errorMessage = this.toolErrMsgs.errWhenStringifying(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
return {}
|
||||
};
|
||||
|
||||
@@ -712,7 +757,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, callThisToolFirst.mcpServerName, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
|
||||
if (interrupted) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
this._addUserCheckpoint({ threadId })
|
||||
@@ -749,8 +794,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
shouldRetryLLM = false
|
||||
nAttempts += 1
|
||||
|
||||
let resMessageIsDonePromise: (res: { type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<{ type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }>((res, rej) => { resMessageIsDonePromise = res })
|
||||
type ResTypes =
|
||||
| { type: 'llmDone', toolCall?: RawToolCallObj, info: { fullText: string, fullReasoning: string, anthropicReasoning: AnthropicReasoning[] | null } }
|
||||
| { type: 'llmError', error?: { message: string; fullError: Error | null; } }
|
||||
| { type: 'llmAborted' }
|
||||
|
||||
let resMessageIsDonePromise: (res: ResTypes) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<ResTypes>((res, rej) => { resMessageIsDonePromise = res })
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
@@ -765,9 +815,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) })
|
||||
},
|
||||
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
|
||||
resMessageIsDonePromise({ type: 'llmDone', toolCall }) // resolve with tool calls
|
||||
|
||||
resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls
|
||||
},
|
||||
onError: async (error) => {
|
||||
resMessageIsDonePromise({ type: 'llmError', error: error })
|
||||
@@ -818,7 +866,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
const { error } = llmRes
|
||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) })
|
||||
|
||||
this._setStreamState(threadId, { isRunning: undefined, error })
|
||||
this._addUserCheckpoint({ threadId })
|
||||
@@ -826,15 +874,19 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// llm res success
|
||||
const { toolCall } = llmRes
|
||||
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
|
||||
const { toolCall, info } = llmRes
|
||||
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: info.fullText, reasoning: info.fullReasoning, anthropicReasoning: info.anthropicReasoning })
|
||||
|
||||
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative for clarity
|
||||
|
||||
// call tool if there is one
|
||||
if (toolCall) {
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
|
||||
const mcpTools = this._mcpService.getMCPTools()
|
||||
const mcpTool = mcpTools?.find(t => t.name === toolCall.name)
|
||||
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, mcpTool?.mcpServerName, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
|
||||
if (interrupted) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
return
|
||||
@@ -1294,7 +1346,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
// URIs of files that have been read
|
||||
else if (m.role === 'tool' && m.type === 'success' && m.name === 'read_file') {
|
||||
const params = m.params as ToolCallParams['read_file']
|
||||
const params = m.params as BuiltinToolCallParams['read_file']
|
||||
addURI(params.uri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
|
||||
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
|
||||
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
|
||||
import { reParsedToolXMLString, chat_systemMessage } from '../common/prompt/prompts.js';
|
||||
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
|
||||
@@ -16,7 +16,10 @@ import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||
import { ToolName } from '../common/toolsServiceTypes.js';
|
||||
import { IMCPService } from '../common/mcpService.js';
|
||||
|
||||
export const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +39,6 @@ type SimpleLLMMessage = {
|
||||
}
|
||||
|
||||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
|
||||
const TRIM_TO_LEN = 120
|
||||
@@ -237,18 +239,6 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export type GeminiMessage = {
|
||||
role: 'user' | 'model'; // Gemini uses 'user' and 'model' roles
|
||||
parts: (
|
||||
| { text: string; }
|
||||
| { functionCall: { tool_call: any } }
|
||||
| { functionResponse: { name: ToolName, response: { result: string } } }
|
||||
)[];
|
||||
};
|
||||
|
||||
|
||||
// --- CHAT ---
|
||||
|
||||
const prepareOpenAIOrAnthropicMessages = ({
|
||||
@@ -417,14 +407,25 @@ const prepareOpenAIOrAnthropicMessages = ({
|
||||
|
||||
|
||||
// ================ no empty message ================
|
||||
for (const currMsg of llmMessages) {
|
||||
for (let i = 0; i < llmMessages.length; i += 1) {
|
||||
const currMsg: AnthropicOrOpenAILLMMessage = llmMessages[i]
|
||||
const nextMsg: AnthropicOrOpenAILLMMessage | undefined = llmMessages[i + 1]
|
||||
|
||||
if (currMsg.role === 'tool') continue
|
||||
|
||||
// if content is a string, replace string with empty msg
|
||||
if (typeof currMsg.content === 'string')
|
||||
if (typeof currMsg.content === 'string') {
|
||||
currMsg.content = currMsg.content || EMPTY_MESSAGE
|
||||
}
|
||||
else {
|
||||
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
|
||||
// allowed to be empty if has a tool in it or following it
|
||||
if (currMsg.content.find(c => c.type === 'tool_result' || c.type === 'tool_use')) {
|
||||
currMsg.content = currMsg.content.filter(c => !(c.type === 'text' && !c.text)) as any
|
||||
continue
|
||||
}
|
||||
if (nextMsg?.role === 'tool') continue
|
||||
|
||||
// replace any empty text entries with empty msg, and make sure there's at least 1 entry
|
||||
for (const c of currMsg.content) {
|
||||
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
|
||||
}
|
||||
@@ -456,8 +457,8 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
|
||||
return { text: c.text }
|
||||
}
|
||||
else if (c.type === 'tool_use') {
|
||||
latestToolName = c.name as ToolName
|
||||
return { functionCall: { name: c.name as ToolName, args: c.input } }
|
||||
latestToolName = c.name
|
||||
return { functionCall: { id: c.id, name: c.name, args: c.input } }
|
||||
}
|
||||
else return null
|
||||
}).filter(m => !!m)
|
||||
@@ -475,7 +476,7 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
|
||||
}
|
||||
else if (c.type === 'tool_result') {
|
||||
if (!latestToolName) return null
|
||||
return { functionResponse: { name: latestToolName, response: { result: c.content } } }
|
||||
return { functionResponse: { id: c.tool_use_id, name: latestToolName, response: { output: c.content } } }
|
||||
}
|
||||
else return null
|
||||
}).filter(m => !!m)
|
||||
@@ -539,6 +540,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
||||
@IMCPService private readonly mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -588,8 +590,10 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
|
||||
const includeXMLToolDefinitions = !specialToolFormat
|
||||
|
||||
const mcpTools = this.mcpService.getMCPTools()
|
||||
|
||||
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions })
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, mcpTools, includeXMLToolDefinitions })
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
@@ -674,13 +678,15 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName, overridesOfModel)
|
||||
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
|
||||
|
||||
const { disableSystemMessage } = this.voidSettingsService.state.globalSettings;
|
||||
const fullSystemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
|
||||
const systemMessage = disableSystemMessage ? '' : fullSystemMessage;
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
|
||||
// Get combined AI instructions
|
||||
const aiInstructions = this._getCombinedAIInstructions();
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions, overridesOfModel)
|
||||
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
|
||||
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { env } from '../../../../base/common/process.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
@@ -29,6 +30,7 @@ export const IExtensionTransferService = createDecorator<IExtensionTransferServi
|
||||
const extensionBlacklist = [
|
||||
// ignore extensions
|
||||
'ms-vscode-remote.remote', // ms-vscode-remote.remote-ssh, ms-vscode-remote.remote-wsl
|
||||
'ms-vscode.remote', // ms-vscode.remote-explorer
|
||||
// ignore other AI copilots that could conflict with Void keybindings
|
||||
'sourcegraph.cody-ai',
|
||||
'continue.continue',
|
||||
@@ -40,6 +42,9 @@ const extensionBlacklist = [
|
||||
];
|
||||
|
||||
|
||||
const isBlacklisted = (fsPath: string | undefined) => {
|
||||
return extensionBlacklist.find(bItem => fsPath?.includes(bItem))
|
||||
}
|
||||
|
||||
class ExtensionTransferService extends Disposable implements IExtensionTransferService {
|
||||
_serviceBrand: undefined;
|
||||
@@ -87,15 +92,31 @@ class ExtensionTransferService extends Disposable implements IExtensionTransferS
|
||||
await fileService.createFolder(toParent)
|
||||
}
|
||||
for (const extensionFolder of stat.children ?? []) {
|
||||
if (extensionBlacklist.find(bItem => extensionFolder.resource.path.includes(bItem))) {
|
||||
console.log('Skipping...', extensionFolder.resource.path)
|
||||
continue
|
||||
}
|
||||
const from = extensionFolder.resource
|
||||
const to = URI.joinPath(toParent, extensionFolder.name)
|
||||
await fileService.copy(from, to, true)
|
||||
const toStat = await fileService.resolve(from)
|
||||
|
||||
if (toStat.isDirectory) {
|
||||
if (!isBlacklisted(extensionFolder.resource.fsPath)) {
|
||||
await fileService.copy(from, to, true)
|
||||
}
|
||||
}
|
||||
else if (toStat.isFile) {
|
||||
if (extensionFolder.name === 'extensions.json') {
|
||||
try {
|
||||
const contentsStr = await fileService.readFile(from)
|
||||
const json: any = JSON.parse(contentsStr.value.toString())
|
||||
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
|
||||
const jsonStr = JSON.stringify(j2)
|
||||
await fileService.writeFile(to, VSBuffer.fromString(jsonStr))
|
||||
}
|
||||
catch {
|
||||
console.log('Error copying extensions.json, skipping')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure the destination directory exists
|
||||
|
||||
} else {
|
||||
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
|
||||
}
|
||||
@@ -113,19 +134,38 @@ class ExtensionTransferService extends Disposable implements IExtensionTransferS
|
||||
}
|
||||
|
||||
async deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null) {
|
||||
const fileService = this._fileService
|
||||
const extensionsURI = getExtensionsFolder(os)
|
||||
if (!extensionsURI) return
|
||||
const eURI = await this._fileService.resolve(extensionsURI)
|
||||
const eURI = await fileService.resolve(extensionsURI)
|
||||
for (const child of eURI.children ?? []) {
|
||||
|
||||
// if is not blacklisted, continue
|
||||
if (!extensionBlacklist.find(bItem => child.resource.path.includes(bItem))) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Deleting extension', child.resource.fsPath)
|
||||
await this._fileService.del(child.resource, { recursive: true, useTrash: true })
|
||||
if (child.isDirectory) {
|
||||
// if is blacklisted
|
||||
if (isBlacklisted(child.resource.fsPath)) {
|
||||
console.log('Deleting extension', child.resource.fsPath)
|
||||
await fileService.del(child.resource, { recursive: true, useTrash: true })
|
||||
}
|
||||
}
|
||||
else if (child.isFile) {
|
||||
// if is extensions.json
|
||||
|
||||
if (child.name === 'extensions.json') {
|
||||
console.log('Updating extensions.json', child.resource.fsPath)
|
||||
try {
|
||||
const contentsStr = await fileService.readFile(child.resource)
|
||||
const json: any = JSON.parse(contentsStr.value.toString())
|
||||
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
|
||||
const jsonStr = JSON.stringify(j2)
|
||||
await fileService.writeFile(child.resource, VSBuffer.fromString(jsonStr))
|
||||
}
|
||||
catch {
|
||||
console.log('Error copying extensions.json, skipping')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Could not delete extension', child.resource.fsPath, e)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Action2, registerAction2, MenuId } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
|
||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||
import { messageOfSelection } from '../common/prompt/prompts.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
|
||||
|
||||
|
||||
class FilePromptActionService extends Action2 {
|
||||
private static readonly VOID_COPY_FILE_PROMPT_ID = 'void.copyfileprompt'
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FilePromptActionService.VOID_COPY_FILE_PROMPT_ID,
|
||||
title: localize2('voidCopyPrompt', 'Void: Copy Prompt'),
|
||||
menu: [{
|
||||
id: MenuId.ExplorerContext,
|
||||
group: '8_void',
|
||||
order: 1,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, uri: URI): Promise<void> {
|
||||
try {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const clipboardService = accessor.get(IClipboardService)
|
||||
const directoryStrService = accessor.get(IDirectoryStrService)
|
||||
const voidModelService = accessor.get(IVoidModelService)
|
||||
|
||||
const stat = await fileService.stat(uri)
|
||||
|
||||
const folderOpts = {
|
||||
maxChildren: 1000,
|
||||
maxCharsPerFile: 2_000_000,
|
||||
} as const
|
||||
|
||||
let m: string = 'No contents detected'
|
||||
if (stat.isFile) {
|
||||
m = await messageOfSelection({
|
||||
type: 'File',
|
||||
uri,
|
||||
language: (await voidModelService.getModelSafe(uri)).model?.getLanguageId() || '',
|
||||
state: { wasAddedAsCurrentFile: false, },
|
||||
}, {
|
||||
folderOpts,
|
||||
directoryStrService,
|
||||
fileService,
|
||||
})
|
||||
}
|
||||
|
||||
if (stat.isDirectory) {
|
||||
m = await messageOfSelection({
|
||||
type: 'Folder',
|
||||
uri,
|
||||
}, {
|
||||
folderOpts,
|
||||
fileService,
|
||||
directoryStrService,
|
||||
})
|
||||
}
|
||||
|
||||
await clipboardService.writeText(m)
|
||||
|
||||
} catch (error) {
|
||||
const notificationService = accessor.get(INotificationService)
|
||||
notificationService.error(error + '')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerAction2(FilePromptActionService)
|
||||
@@ -5,17 +5,19 @@
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
// import { IExtensionTransferService } from './extensionTransferService.js';
|
||||
// import { os } from '../common/helpers/systemInfo.js';
|
||||
// import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IExtensionTransferService } from './extensionTransferService.js';
|
||||
import { os } from '../common/helpers/systemInfo.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { timeout } from '../../../../base/common/async.js';
|
||||
import { getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
|
||||
// Onboarding contribution that mounts the component at startup
|
||||
export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.voidMiscWorkbenchContribs';
|
||||
|
||||
constructor(
|
||||
// @IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService,
|
||||
// @IStorageService private readonly storageService: IStorageService,
|
||||
@IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.initialize();
|
||||
@@ -23,13 +25,23 @@ export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContr
|
||||
|
||||
private initialize(): void {
|
||||
|
||||
// // delete blacklisted extensions once (this is for people who already installed them)
|
||||
// const deleteExtensionsStorageId = 'void-deleted-blacklist'
|
||||
// const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION)
|
||||
// if (!alreadyDeleted) {
|
||||
// this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
// this.extensionTransferService.deleteBlacklistExtensions(os)
|
||||
// }
|
||||
// delete blacklisted extensions once (this is for people who already installed them)
|
||||
const deleteExtensionsStorageId = 'void-deleted-blacklist-2'
|
||||
const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION)
|
||||
if (!alreadyDeleted) {
|
||||
this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
this.extensionTransferService.deleteBlacklistExtensions(os)
|
||||
}
|
||||
|
||||
|
||||
// after some time, trigger a resize event for the blank screen error
|
||||
timeout(5_000).then(() => {
|
||||
// Get the active window reference for multi-window support
|
||||
const targetWindow = getActiveWindow();
|
||||
// Trigger a window resize event to ensure proper layout calculations
|
||||
targetWindow.dispatchEvent(new Event('resize'))
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { roundRangeToLines } from './sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
@@ -42,6 +42,7 @@ registerAction2(class extends Action2 {
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
when: ContextKeyExpr.deserialize('editorFocus && !terminalFocus'),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+111
-15
@@ -239,27 +239,104 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId:
|
||||
}
|
||||
|
||||
|
||||
export const ApplyButtonsHTML = ({
|
||||
const terminalLanguages = new Set([
|
||||
'bash',
|
||||
'shellscript',
|
||||
'shell',
|
||||
'powershell',
|
||||
'bat',
|
||||
'zsh',
|
||||
'sh',
|
||||
'fish',
|
||||
'nushell',
|
||||
'ksh',
|
||||
'xonsh',
|
||||
'elvish',
|
||||
])
|
||||
|
||||
const ApplyButtonsForTerminal = ({
|
||||
codeStr,
|
||||
applyBoxId,
|
||||
uri,
|
||||
language,
|
||||
}: {
|
||||
codeStr: string,
|
||||
applyBoxId: string,
|
||||
} & ({
|
||||
language?: string,
|
||||
uri: URI | 'current';
|
||||
})
|
||||
) => {
|
||||
}) => {
|
||||
const accessor = useAccessor()
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const terminalToolService = accessor.get('ITerminalToolService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const [isShellRunning, setIsShellRunning] = useState<boolean>(false)
|
||||
const interruptToolRef = useRef<(() => void) | null>(null)
|
||||
const isDisabled = isShellRunning
|
||||
|
||||
const onClickSubmit = useCallback(async () => {
|
||||
if (isShellRunning) return
|
||||
try {
|
||||
setIsShellRunning(true)
|
||||
const terminalId = await terminalToolService.createPersistentTerminal({ cwd: null })
|
||||
const { interrupt } = await terminalToolService.runCommand(
|
||||
codeStr,
|
||||
{ type: 'persistent', persistentTerminalId: terminalId }
|
||||
);
|
||||
interruptToolRef.current = interrupt
|
||||
metricsService.capture('Execute Shell', { length: codeStr.length })
|
||||
} catch (e) {
|
||||
setIsShellRunning(false)
|
||||
console.error('Failed to execute in terminal:', e)
|
||||
}
|
||||
}, [codeStr, uri, applyBoxId, metricsService, terminalToolService, isShellRunning])
|
||||
|
||||
if (isShellRunning) {
|
||||
return (
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={() => {
|
||||
interruptToolRef.current?.();
|
||||
setIsShellRunning(false);
|
||||
}}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isDisabled) {
|
||||
return null
|
||||
}
|
||||
return <IconShell1
|
||||
Icon={Play}
|
||||
onClick={onClickSubmit}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ApplyButtonsForEdit = ({
|
||||
codeStr,
|
||||
applyBoxId,
|
||||
uri,
|
||||
language,
|
||||
}: {
|
||||
codeStr: string,
|
||||
applyBoxId: string,
|
||||
language?: string,
|
||||
uri: URI | 'current';
|
||||
}) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const notificationService = accessor.get('INotificationService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
const { currStreamStateRef, setApplying } = useApplyStreamState({ applyBoxId })
|
||||
|
||||
|
||||
const onClickSubmit = useCallback(async () => {
|
||||
if (currStreamStateRef.current === 'streaming') return
|
||||
|
||||
@@ -271,17 +348,22 @@ export const ApplyButtonsHTML = ({
|
||||
uri: uri,
|
||||
startBehavior: 'reject-conflicts',
|
||||
}) ?? []
|
||||
console.log('setting!!!', newApplyingUri)
|
||||
setApplying(newApplyingUri)
|
||||
|
||||
if (!applyDonePromise) {
|
||||
notificationService.info(`Void Error: We couldn't run Apply here. ${uri === 'current' ? 'This Apply block wants to run on the current file, but you might not have a file open.' : `This Apply block wants to run on ${uri.fsPath}, but it might not exist.`}`)
|
||||
}
|
||||
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
|
||||
notificationService.info(`Void Error: There was a problem running Apply: ${e}.`)
|
||||
|
||||
})
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
|
||||
}, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService])
|
||||
}, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService, notificationService])
|
||||
|
||||
|
||||
const onClickStop = useCallback(() => {
|
||||
@@ -303,10 +385,7 @@ export const ApplyButtonsHTML = ({
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [uri, applyBoxId, editCodeService])
|
||||
|
||||
|
||||
const currStreamState = currStreamStateRef.current
|
||||
console.log('currStreamState...', currStreamState)
|
||||
|
||||
if (currStreamState === 'streaming') {
|
||||
return <IconShell1
|
||||
Icon={Square}
|
||||
@@ -314,12 +393,9 @@ export const ApplyButtonsHTML = ({
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (isDisabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
if (currStreamState === 'idle-no-changes') {
|
||||
return <IconShell1
|
||||
Icon={Play}
|
||||
@@ -327,7 +403,6 @@ export const ApplyButtonsHTML = ({
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
return <Fragment>
|
||||
<IconShell1
|
||||
@@ -348,6 +423,27 @@ export const ApplyButtonsHTML = ({
|
||||
|
||||
|
||||
|
||||
export const ApplyButtonsHTML = (params: {
|
||||
codeStr: string,
|
||||
applyBoxId: string,
|
||||
language?: string,
|
||||
uri: URI | 'current';
|
||||
}) => {
|
||||
const { language } = params
|
||||
const isShellLanguage = !!language && terminalLanguages.has(language)
|
||||
|
||||
if (isShellLanguage) {
|
||||
return <ApplyButtonsForTerminal {...params} />
|
||||
}
|
||||
else {
|
||||
return <ApplyButtonsForEdit {...params} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const EditToolAcceptRejectButtonsHTML = ({
|
||||
codeStr,
|
||||
applyBoxId,
|
||||
@@ -451,7 +547,7 @@ export const BlockCodeApplyWrapper = ({
|
||||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={codeStr} />
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={codeStr} language={language} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { isAbsolute } from '../../../../../../../base/common/path.js'
|
||||
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
|
||||
import { BlockCode } from '../util/inputs.js'
|
||||
import { CodespanLocationLink } from '../../../../common/chatThreadServiceTypes.js'
|
||||
import { voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { getBasename, getRelative, voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
|
||||
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
@@ -89,13 +89,18 @@ const LatexRender = ({ latex }: { latex: string }) => {
|
||||
// }
|
||||
}
|
||||
|
||||
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
||||
const Codespan = ({ text, className, onClick, tooltip }: { text: string, className?: string, onClick?: () => void, tooltip?: string }) => {
|
||||
|
||||
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
|
||||
|
||||
return <code
|
||||
className={`font-mono font-medium rounded-sm bg-void-bg-1 px-1 ${className}`}
|
||||
onClick={onClick}
|
||||
{...tooltip ? {
|
||||
'data-tooltip-id': 'void-tooltip',
|
||||
'data-tooltip-content': tooltip,
|
||||
'data-tooltip-place': 'top',
|
||||
} : {}}
|
||||
>
|
||||
{text}
|
||||
</code>
|
||||
@@ -115,8 +120,11 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
|
||||
|
||||
let link: CodespanLocationLink | undefined = undefined
|
||||
if (rawText.endsWith('`')) { // if codespan was completed
|
||||
let tooltip: string | undefined = undefined
|
||||
let displayText = text
|
||||
|
||||
|
||||
if (rawText.endsWith('`')) {
|
||||
// get link from cache
|
||||
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
|
||||
|
||||
@@ -127,23 +135,33 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
|
||||
setDidComputeCodespanLink(true) // rerender
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if (link?.displayText) {
|
||||
displayText = link.displayText
|
||||
}
|
||||
|
||||
if (isValidUri(displayText)) {
|
||||
tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip
|
||||
displayText = getBasename(displayText)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onClick = () => {
|
||||
if (!link || !link.selection) return;
|
||||
|
||||
if (!link) return;
|
||||
// Use the updated voidOpenFileFn to open the file and handle selection
|
||||
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
|
||||
if (link.selection)
|
||||
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
|
||||
else
|
||||
voidOpenFileFn(link.uri, accessor);
|
||||
}
|
||||
|
||||
return <Codespan
|
||||
text={link?.displayText || text}
|
||||
text={displayText}
|
||||
onClick={onClick}
|
||||
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
|
||||
tooltip={tooltip || undefined}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markd
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch, VoidDiffEditor } from '../util/inputs.js';
|
||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { PastThreadsList } from './SidebarThreadSelector.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
@@ -24,11 +24,11 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
|
||||
import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react';
|
||||
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js';
|
||||
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js';
|
||||
import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { IsRunningType } from '../../../chatThreadService.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } from '../../../../common/prompt/prompts.js';
|
||||
import { builtinToolNames, isABuiltinToolName, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js';
|
||||
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
|
||||
@@ -36,6 +36,7 @@ import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
|
||||
import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
|
||||
|
||||
|
||||
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
@@ -495,12 +496,12 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe
|
||||
);
|
||||
};
|
||||
|
||||
const getRelative = (uri: URI, accessor: ReturnType<typeof useAccessor>) => {
|
||||
export const getRelative = (uri: URI, accessor: ReturnType<typeof useAccessor>) => {
|
||||
const workspaceContextService = accessor.get('IWorkspaceContextService')
|
||||
let path: string
|
||||
const isInside = workspaceContextService.isInsideWorkspace(uri)
|
||||
if (isInside) {
|
||||
const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath.startsWith(f.uri.fsPath))
|
||||
const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath?.startsWith(f.uri.fsPath))
|
||||
if (f) { path = uri.fsPath.replace(f.uri.fsPath, '') }
|
||||
else { path = uri.fsPath }
|
||||
}
|
||||
@@ -907,11 +908,14 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
|
||||
const desc1OnClick = () => voidOpenFileFn(params.uri, accessor)
|
||||
const componentParams: ToolHeaderParams = { title, desc1, desc1OnClick, desc1Info, isError, icon, isRejected, }
|
||||
|
||||
|
||||
const editToolType = toolMessage.name === 'edit_file' ? 'diff' : 'rewrite'
|
||||
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
|
||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
code={content}
|
||||
type={editToolType}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
// JumpToFileButton removed in favor of FileLinkText
|
||||
@@ -936,6 +940,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
code={content}
|
||||
type={editToolType}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
|
||||
@@ -1388,7 +1393,7 @@ const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => {
|
||||
</span>
|
||||
}
|
||||
|
||||
const titleOfToolName = {
|
||||
const titleOfBuiltinToolName = {
|
||||
'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') },
|
||||
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
|
||||
'get_dir_tree': { done: 'Inspected folder tree', proposed: 'Inspect folder tree', running: loadingTitleWrapper('Inspecting folder tree') },
|
||||
@@ -1406,21 +1411,42 @@ const titleOfToolName = {
|
||||
|
||||
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
|
||||
'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') },
|
||||
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
|
||||
} as const satisfies Record<BuiltinToolName, { done: any, proposed: any, running: any }>
|
||||
|
||||
|
||||
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type'>): React.ReactNode => {
|
||||
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type' | 'mcpServerName'>): React.ReactNode => {
|
||||
const t = toolMessage
|
||||
if (!toolNames.includes(t.name as ToolName)) return t.name // good measure
|
||||
|
||||
const toolName = t.name as ToolName
|
||||
if (t.type === 'success') return titleOfToolName[toolName].done
|
||||
if (t.type === 'running_now') return titleOfToolName[toolName].running
|
||||
return titleOfToolName[toolName].proposed
|
||||
// non-built-in title
|
||||
if (!builtinToolNames.includes(t.name as BuiltinToolName)) {
|
||||
// descriptor of Running or Ran etc
|
||||
const descriptor =
|
||||
t.type === 'success' ? 'Called'
|
||||
: t.type === 'running_now' ? 'Calling'
|
||||
: t.type === 'tool_request' ? 'Call'
|
||||
: t.type === 'rejected' ? 'Call'
|
||||
: t.type === 'invalid_params' ? 'Call'
|
||||
: t.type === 'tool_error' ? 'Call'
|
||||
: 'Call'
|
||||
|
||||
|
||||
const title = `${descriptor} ${toolMessage.mcpServerName || 'MCP'}`
|
||||
if (t.type === 'running_now' || t.type === 'tool_request')
|
||||
return loadingTitleWrapper(title)
|
||||
return title
|
||||
}
|
||||
|
||||
// built-in title
|
||||
else {
|
||||
const toolName = t.name as BuiltinToolName
|
||||
if (t.type === 'success') return titleOfBuiltinToolName[toolName].done
|
||||
if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running
|
||||
return titleOfBuiltinToolName[toolName].proposed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
|
||||
const toolNameToDesc = (toolName: BuiltinToolName, _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
|
||||
desc1: React.ReactNode,
|
||||
desc1Info?: string,
|
||||
} => {
|
||||
@@ -1431,95 +1457,95 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
|
||||
|
||||
const x = {
|
||||
'read_file': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['read_file']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['read_file']
|
||||
return {
|
||||
desc1: getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
};
|
||||
},
|
||||
'ls_dir': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['ls_dir']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['ls_dir']
|
||||
return {
|
||||
desc1: getFolderName(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
};
|
||||
},
|
||||
'search_pathnames_only': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['search_pathnames_only']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['search_pathnames_only']
|
||||
return {
|
||||
desc1: `"${toolParams.query}"`,
|
||||
}
|
||||
},
|
||||
'search_for_files': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['search_for_files']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['search_for_files']
|
||||
return {
|
||||
desc1: `"${toolParams.query}"`,
|
||||
}
|
||||
},
|
||||
'search_in_file': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['search_in_file'];
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['search_in_file'];
|
||||
return {
|
||||
desc1: `"${toolParams.query}"`,
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
};
|
||||
},
|
||||
'create_file_or_folder': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['create_file_or_folder']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['create_file_or_folder']
|
||||
return {
|
||||
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'delete_file_or_folder': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['delete_file_or_folder']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['delete_file_or_folder']
|
||||
return {
|
||||
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'rewrite_file': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['rewrite_file']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['rewrite_file']
|
||||
return {
|
||||
desc1: getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'edit_file': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['edit_file']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['edit_file']
|
||||
return {
|
||||
desc1: getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'run_command': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['run_command']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['run_command']
|
||||
return {
|
||||
desc1: `"${toolParams.command}"`,
|
||||
}
|
||||
},
|
||||
'run_persistent_command': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['run_persistent_command']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['run_persistent_command']
|
||||
return {
|
||||
desc1: `"${toolParams.command}"`,
|
||||
}
|
||||
},
|
||||
'open_persistent_terminal': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['open_persistent_terminal']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['open_persistent_terminal']
|
||||
return { desc1: '' }
|
||||
},
|
||||
'kill_persistent_terminal': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['kill_persistent_terminal']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['kill_persistent_terminal']
|
||||
return { desc1: toolParams.persistentTerminalId }
|
||||
},
|
||||
'get_dir_tree': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['get_dir_tree']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['get_dir_tree']
|
||||
return {
|
||||
desc1: getFolderName(toolParams.uri.fsPath) ?? '/',
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'read_lint_errors': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['read_lint_errors']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['read_lint_errors']
|
||||
return {
|
||||
desc1: getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
@@ -1590,9 +1616,9 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
|
||||
</button>
|
||||
)
|
||||
|
||||
const approvalType = approvalTypeOfToolName[toolName]
|
||||
const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools'
|
||||
const approvalToggle = approvalType ? <div key={approvalType} className="flex items-center ml-2 gap-x-1">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc='Auto-approve' />
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
</div> : null
|
||||
|
||||
return <div className="flex gap-2 mx-0.5 items-center">
|
||||
@@ -1604,7 +1630,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
|
||||
|
||||
export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <div className={`${className ? className : ''} cursor-default select-none`}>
|
||||
<div className='px-2 min-w-full'>
|
||||
<div className='px-2 min-w-full overflow-hidden'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1633,12 +1659,18 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
|
||||
|
||||
|
||||
|
||||
const EditToolChildren = ({ uri, code }: { uri: URI | undefined, code: string }) => {
|
||||
const EditToolChildren = ({ uri, code, type }: { uri: URI | undefined, code: string, type: 'diff' | 'rewrite' }) => {
|
||||
|
||||
const content = type === 'diff' ?
|
||||
<VoidDiffEditor uri={uri} searchReplaceBlocks={code} />
|
||||
: <ChatMarkdownRender string={`\`\`\`\n${code}\n\`\`\``} codeURI={uri} chatMessageLocation={undefined} />
|
||||
|
||||
return <div className='!select-text cursor-auto'>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender string={code} codeURI={uri} chatMessageLocation={undefined} />
|
||||
{content}
|
||||
</SmallProseWrapper>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1651,8 +1683,8 @@ const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
|
||||
}
|
||||
|
||||
const BottomChildren = ({ children, title }: { children: React.ReactNode, title: string }) => {
|
||||
if (!children) return null;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
if (!children) return null;
|
||||
return (
|
||||
<div className="w-full px-2 mt-0.5">
|
||||
<div
|
||||
@@ -1689,9 +1721,9 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr, toolName, threadId }:
|
||||
|
||||
|
||||
|
||||
const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: string }) => {
|
||||
const InvalidTool = ({ toolName, message, mcpServerName }: { toolName: ToolName, message: string, mcpServerName: string | undefined }) => {
|
||||
const accessor = useAccessor()
|
||||
const title = getTitle({ name: toolName, type: 'invalid_params' })
|
||||
const title = getTitle({ name: toolName, type: 'invalid_params', mcpServerName })
|
||||
const desc1 = 'Invalid parameters'
|
||||
const icon = null
|
||||
const isError = true
|
||||
@@ -1705,9 +1737,9 @@ const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: strin
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
}
|
||||
|
||||
const CanceledTool = ({ toolName }: { toolName: ToolName }) => {
|
||||
const CanceledTool = ({ toolName, mcpServerName }: { toolName: ToolName, mcpServerName: string | undefined }) => {
|
||||
const accessor = useAccessor()
|
||||
const title = getTitle({ name: toolName, type: 'rejected' })
|
||||
const title = getTitle({ name: toolName, type: 'rejected', mcpServerName })
|
||||
const desc1 = ''
|
||||
const icon = null
|
||||
const isRejected = true
|
||||
@@ -1819,9 +1851,61 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({
|
||||
</>
|
||||
}
|
||||
|
||||
type WrapperProps<T extends ToolName> = { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }
|
||||
const MCPToolWrapper = ({ toolMessage }: WrapperProps<string>) => {
|
||||
const accessor = useAccessor()
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
|
||||
type ResultWrapper<T extends ToolName> = (props: { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode
|
||||
const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>, } } = {
|
||||
const title = getTitle(toolMessage)
|
||||
const desc1 = toolMessage.name
|
||||
const icon = null
|
||||
|
||||
|
||||
if (toolMessage.type === 'running_now') return null // do not show running
|
||||
|
||||
const isError = false
|
||||
const isRejected = toolMessage.type === 'rejected'
|
||||
const { rawParams, params } = toolMessage
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, }
|
||||
|
||||
const paramsStr = JSON.stringify(params, null, 2)
|
||||
componentParams.desc2 = <CopyButton codeStr={paramsStr} toolTipName={`Copy inputs: ${paramsStr}`} />
|
||||
|
||||
componentParams.info = !toolMessage.mcpServerName ? 'MCP tool not found' : undefined
|
||||
|
||||
// Add copy inputs button in desc2
|
||||
|
||||
|
||||
if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') {
|
||||
const { result } = toolMessage
|
||||
const resultStr = result ? mcpService.stringifyResult(result) : 'null'
|
||||
componentParams.children = <ToolChildrenWrapper>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender
|
||||
string={`\`\`\`json\n${resultStr}\n\`\`\``}
|
||||
chatMessageLocation={undefined}
|
||||
isApplyEnabled={false}
|
||||
isLinkDetectionEnabled={true}
|
||||
/>
|
||||
</SmallProseWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else if (toolMessage.type === 'tool_error') {
|
||||
const { result } = toolMessage
|
||||
componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||
<CodeChildren>
|
||||
{result}
|
||||
</CodeChildren>
|
||||
</BottomChildren>
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
|
||||
}
|
||||
|
||||
type ResultWrapper<T extends ToolName> = (props: WrapperProps<T>) => React.ReactNode
|
||||
|
||||
const builtinToolNameToComponent: { [T in BuiltinToolName]: { resultWrapper: ResultWrapper<T>, } } = {
|
||||
'read_file': {
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
@@ -1905,7 +1989,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
</SmallProseWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else {
|
||||
else if (toolMessage.type === 'tool_error') {
|
||||
const { result } = toolMessage
|
||||
componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||
<CodeChildren>
|
||||
@@ -1960,7 +2044,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
}
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else {
|
||||
else if (toolMessage.type === 'tool_error') {
|
||||
const { result } = toolMessage
|
||||
componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||
<CodeChildren>
|
||||
@@ -2009,7 +2093,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else {
|
||||
else if (toolMessage.type === 'tool_error') {
|
||||
const { result } = toolMessage
|
||||
componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||
<CodeChildren>
|
||||
@@ -2064,7 +2148,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else {
|
||||
else if (toolMessage.type === 'tool_error') {
|
||||
const { result } = toolMessage
|
||||
componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||
<CodeChildren>
|
||||
@@ -2257,12 +2341,12 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||
},
|
||||
'rewrite_file': {
|
||||
resultWrapper: (params) => {
|
||||
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.newContent}${'\n```'}`} />
|
||||
return <EditTool {...params} content={params.toolMessage.params.newContent} />
|
||||
}
|
||||
},
|
||||
'edit_file': {
|
||||
resultWrapper: (params) => {
|
||||
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.searchReplaceBlocks}${'\n```'}`} />
|
||||
return <EditTool {...params} content={params.toolMessage.params.searchReplaceBlocks} />
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2442,11 +2526,15 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
|
||||
|
||||
if (chatMessage.type === 'invalid_params') {
|
||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} />
|
||||
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} mcpServerName={chatMessage.mcpServerName} />
|
||||
</div>
|
||||
}
|
||||
|
||||
const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper<ToolName>
|
||||
const toolName = chatMessage.name
|
||||
const isBuiltInTool = isABuiltinToolName(toolName)
|
||||
const ToolResultWrapper = isBuiltInTool ? builtinToolNameToComponent[toolName]?.resultWrapper as ResultWrapper<ToolName>
|
||||
: MCPToolWrapper as ResultWrapper<ToolName>
|
||||
|
||||
if (ToolResultWrapper)
|
||||
return <>
|
||||
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
@@ -2466,7 +2554,7 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
|
||||
|
||||
else if (role === 'interrupted_streaming_tool') {
|
||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<CanceledTool toolName={chatMessage.name} />
|
||||
<CanceledTool toolName={chatMessage.name} mcpServerName={chatMessage.mcpServerName} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2605,7 +2693,7 @@ const CommandBarInChat = () => {
|
||||
|
||||
|
||||
// !select-text cursor-auto
|
||||
const fileDetailsContent = <div className="px-2 gap-1 w-full">
|
||||
const fileDetailsContent = <div className="px-2 gap-1 w-full overflow-y-auto">
|
||||
{sortedCommandBarURIs.map((uri, i) => {
|
||||
const basename = getBasename(uri.fsPath)
|
||||
|
||||
@@ -2746,12 +2834,13 @@ const CommandBarInChat = () => {
|
||||
|
||||
const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => {
|
||||
|
||||
if (!isABuiltinToolName(toolCallSoFar.name)) return null
|
||||
|
||||
const accessor = useAccessor()
|
||||
|
||||
const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined
|
||||
|
||||
const title = titleOfToolName[toolCallSoFar.name].proposed
|
||||
const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed
|
||||
|
||||
const uriDone = toolCallSoFar.doneParams.includes('uri')
|
||||
const desc1 = <span className='flex items-center'>
|
||||
@@ -2772,12 +2861,11 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
|
||||
<EditToolChildren
|
||||
uri={uri}
|
||||
code={toolCallSoFar.rawParams.search_replace_blocks ?? toolCallSoFar.rawParams.new_content ?? ''}
|
||||
type={'rewrite'} // as it streams, show in rewrite format, don't make a diff editor
|
||||
/>
|
||||
<IconLoading />
|
||||
</ToolHeaderWrapper>
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2862,6 +2950,7 @@ export const SidebarChat = () => {
|
||||
textAreaRef: textAreaRef,
|
||||
scrollToBottom: () => scrollToBottom(scrollContainerRef),
|
||||
})
|
||||
|
||||
}, [chatThreadsState, threadId, textAreaRef, scrollContainerRef, isResolved])
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
|
||||
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { DiffEditorWidget } from '../../../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';
|
||||
import { extractSearchReplaceBlocks, ExtractedSearchReplaceBlock } from '../../../../common/helpers/extractCodeFromResult.js';
|
||||
import { IAccessibilitySignalService } from '../../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
|
||||
import { IEditorProgressService } from '../../../../../../../platform/progress/common/progress.js';
|
||||
import { detectLanguage } from '../../../../common/helpers/languageHelpers.js';
|
||||
|
||||
|
||||
// type guard
|
||||
@@ -132,7 +137,7 @@ const scoreSubsequence = (text: string, pattern: string): number => {
|
||||
}
|
||||
|
||||
|
||||
export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
|
||||
function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
|
||||
const workspaceService = accessor.get('IWorkspaceContextService');
|
||||
const workspaceFolders = workspaceService.getWorkspace().folders;
|
||||
|
||||
@@ -951,11 +956,11 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
return <WidgetComponent
|
||||
ctor={InputBox}
|
||||
className='
|
||||
bg-void-bg-1
|
||||
@@void-force-child-placeholder-void-fg-1
|
||||
'
|
||||
ctor={InputBox}
|
||||
propsFn={useCallback((container) => [
|
||||
container,
|
||||
contextViewProvider,
|
||||
@@ -991,8 +996,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||
inputBoxRef.current = instance;
|
||||
|
||||
return disposables
|
||||
}, [onChangeText, onCreateInstance, inputBoxRef])
|
||||
}
|
||||
}, [onChangeText, onCreateInstance, inputBoxRef])}
|
||||
/>
|
||||
};
|
||||
|
||||
@@ -1841,3 +1845,154 @@ export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: {
|
||||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
const SingleDiffEditor = ({ block, lang }: { block: ExtractedSearchReplaceBlock, lang: string | undefined }) => {
|
||||
const accessor = useAccessor();
|
||||
const modelService = accessor.get('IModelService');
|
||||
const instantiationService = accessor.get('IInstantiationService');
|
||||
const languageService = accessor.get('ILanguageService');
|
||||
|
||||
const languageSelection = useMemo(() => languageService.createById(lang), [lang, languageService]);
|
||||
|
||||
// Create models for original and modified
|
||||
const originalModel = useMemo(() =>
|
||||
modelService.createModel(block.orig, languageSelection),
|
||||
[block.orig, languageSelection, modelService]
|
||||
);
|
||||
const modifiedModel = useMemo(() =>
|
||||
modelService.createModel(block.final, languageSelection),
|
||||
[block.final, languageSelection, modelService]
|
||||
);
|
||||
|
||||
// Clean up models on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
originalModel.dispose();
|
||||
modifiedModel.dispose();
|
||||
};
|
||||
}, [originalModel, modifiedModel]);
|
||||
|
||||
// Imperatively mount the DiffEditorWidget
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!divRef.current) return;
|
||||
// Create the diff editor instance
|
||||
const editor = instantiationService.createInstance(
|
||||
DiffEditorWidget,
|
||||
divRef.current,
|
||||
{
|
||||
automaticLayout: true,
|
||||
readOnly: true,
|
||||
renderSideBySide: true,
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'off',
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'auto',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 8,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
ignoreHorizontalScrollbarInContentHeight: true,
|
||||
},
|
||||
hover: { enabled: false },
|
||||
folding: false,
|
||||
selectionHighlight: false,
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
glyphMargin: false,
|
||||
stickyScroll: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
renderGutterMenu: false,
|
||||
renderIndicators: false,
|
||||
},
|
||||
{ originalEditor: { isSimpleWidget: true }, modifiedEditor: { isSimpleWidget: true } }
|
||||
);
|
||||
editor.setModel({ original: originalModel, modified: modifiedModel });
|
||||
|
||||
// Calculate the height based on content
|
||||
const updateHeight = () => {
|
||||
const contentHeight = Math.max(
|
||||
originalModel.getLineCount() * 19, // approximate line height
|
||||
modifiedModel.getLineCount() * 19
|
||||
) + 19 * 2 + 1; // add padding
|
||||
|
||||
// Set reasonable min/max heights
|
||||
const height = Math.min(Math.max(contentHeight, 100), 300);
|
||||
if (divRef.current) {
|
||||
divRef.current.style.height = `${height}px`;
|
||||
editor.layout();
|
||||
}
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
editorRef.current = editor;
|
||||
|
||||
// Update height when content changes
|
||||
const disposable1 = originalModel.onDidChangeContent(() => updateHeight());
|
||||
const disposable2 = modifiedModel.onDidChangeContent(() => updateHeight());
|
||||
|
||||
return () => {
|
||||
disposable1.dispose();
|
||||
disposable2.dispose();
|
||||
editor.dispose();
|
||||
editorRef.current = null;
|
||||
};
|
||||
}, [originalModel, modifiedModel, instantiationService]);
|
||||
|
||||
return (
|
||||
<div className="w-full bg-void-bg-3 @@bg-editor-style-override" ref={divRef} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* ToolDiffEditor mounts a native VSCode DiffEditorWidget to show a diff between original and modified code blocks.
|
||||
* Props:
|
||||
* - uri: URI of the file (for language detection, etc)
|
||||
* - searchReplaceBlocks: string in search/replace format (from LLM)
|
||||
* - language?: string (optional, fallback to 'plaintext')
|
||||
*/
|
||||
export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: any, searchReplaceBlocks: string, language?: string }) => {
|
||||
const accessor = useAccessor();
|
||||
const languageService = accessor.get('ILanguageService');
|
||||
|
||||
// Extract all blocks
|
||||
const blocks = extractSearchReplaceBlocks(searchReplaceBlocks);
|
||||
|
||||
// Use detectLanguage for language detection if not provided
|
||||
let lang = language;
|
||||
if (!lang && blocks.length > 0) {
|
||||
lang = detectLanguage(languageService, { uri: uri ?? null, fileContents: blocks[0].orig });
|
||||
}
|
||||
|
||||
// If no blocks, show empty state
|
||||
if (blocks.length === 0) {
|
||||
return <div className="w-full p-4 text-void-fg-4 text-sm">No changes found</div>;
|
||||
}
|
||||
|
||||
// Display all blocks
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
{blocks.map((block, index) => (
|
||||
<div key={index} className="w-full">
|
||||
{blocks.length > 1 && (
|
||||
<div className="text-void-fg-4 text-xs mb-1 px-1">
|
||||
Change {index + 1} of {blocks.length}
|
||||
</div>
|
||||
)}
|
||||
<SingleDiffEditor block={block} lang={lang} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
@@ -51,6 +51,7 @@ import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService
|
||||
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
|
||||
import { ISearchService } from '../../../../../../services/search/common/search.js'
|
||||
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
|
||||
import { IMCPService } from '../../../../common/mcpService.js';
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
@@ -78,6 +79,8 @@ const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) =>
|
||||
const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set();
|
||||
const activeURIListeners: Set<(uri: URI | null) => void> = new Set();
|
||||
|
||||
const mcpListeners: Set<() => 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!
|
||||
@@ -95,9 +98,10 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
editCodeService: accessor.get(IEditCodeService),
|
||||
voidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||
modelService: accessor.get(IModelService),
|
||||
mcpService: accessor.get(IMCPService),
|
||||
}
|
||||
|
||||
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
|
||||
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService, mcpService } = stateServices
|
||||
|
||||
|
||||
|
||||
@@ -164,6 +168,11 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
})
|
||||
)
|
||||
|
||||
disposables.push(
|
||||
mcpService.onDidChangeState(() => {
|
||||
mcpListeners.forEach(l => l())
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
return disposables
|
||||
@@ -215,6 +224,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
ITerminalService: accessor.get(ITerminalService),
|
||||
IExtensionManagementService: accessor.get(IExtensionManagementService),
|
||||
IExtensionTransferService: accessor.get(IExtensionTransferService),
|
||||
IMCPService: accessor.get(IMCPService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
@@ -376,3 +386,16 @@ export const useActiveURI = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
export const useMCPServiceState = () => {
|
||||
const accessor = useAccessor()
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
const [s, ss] = useState(mcpService.state)
|
||||
useEffect(() => {
|
||||
const listener = () => { ss(mcpService.state) }
|
||||
mcpListeners.add(listener);
|
||||
return () => { mcpListeners.delete(listener) };
|
||||
}, []);
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ const tabNames = ['Free', 'Paid', 'Local'] as const;
|
||||
type TabName = typeof tabNames[number] | 'Cloud/Other';
|
||||
|
||||
// Data for cloud providers tab
|
||||
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'openAICompatible'];
|
||||
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'awsBedrock', 'openAICompatible'];
|
||||
|
||||
// Data structures for provider tabs
|
||||
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
|
||||
|
||||
@@ -17,8 +17,20 @@ import { os } from '../../../../common/helpers/systemInfo.js'
|
||||
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'
|
||||
import Severity from '../../../../../../../base/common/severity.js'
|
||||
import { getModelCapabilities, ModelOverrides } from '../../../../common/modelCapabilities.js';
|
||||
import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../../../common/modelCapabilities.js';
|
||||
import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js';
|
||||
import { MCPServer } from '../../../../common/mcpServiceTypes.js';
|
||||
import { useMCPServiceState } from '../util/services.js';
|
||||
|
||||
type Tab =
|
||||
| 'models'
|
||||
| 'localProviders'
|
||||
| 'providers'
|
||||
| 'featureOptions'
|
||||
| 'mcp'
|
||||
| 'general'
|
||||
| 'all';
|
||||
|
||||
|
||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||
|
||||
@@ -185,6 +197,11 @@ const ConfirmButton = ({ children, onConfirm, className }: { children: React.Rea
|
||||
};
|
||||
|
||||
// ---------------- Simplified Model Settings Dialog ------------------
|
||||
|
||||
// keys of ModelOverrides we allow the user to override
|
||||
|
||||
|
||||
|
||||
// This new dialog replaces the verbose UI with a single JSON override box.
|
||||
const SimpleModelSettingsDialog = ({
|
||||
isOpen,
|
||||
@@ -208,39 +225,26 @@ const SimpleModelSettingsDialog = ({
|
||||
const currentOverrides = settingsState.overridesOfModel?.[providerName]?.[modelName] ?? undefined;
|
||||
const { recognizedModelName, isUnrecognizedModel } = defaultModelCapabilities
|
||||
|
||||
// keys of ModelOverrides we allow the user to override
|
||||
const allowedKeys: (string & (keyof ModelOverrides))[] = [
|
||||
'contextWindow',
|
||||
'reservedOutputTokenSpace',
|
||||
'supportsSystemMessage',
|
||||
'specialToolFormat',
|
||||
'supportsFIM',
|
||||
'reasoningCapabilities',
|
||||
];
|
||||
|
||||
// Create the placeholder with the default values for allowed keys
|
||||
const partialDefaults: Partial<ModelOverrides> = {};
|
||||
for (const k of allowedKeys) { if (defaultModelCapabilities[k]) partialDefaults[k] = defaultModelCapabilities[k] as any; }
|
||||
for (const k of modelOverrideKeys) { if (defaultModelCapabilities[k]) partialDefaults[k] = defaultModelCapabilities[k] as any; }
|
||||
const placeholder = JSON.stringify(partialDefaults, null, 2);
|
||||
|
||||
const [overrideEnabled, setOverrideEnabled] = useState<boolean>(() => !!currentOverrides);
|
||||
const [jsonText, setJsonText] = useState<string>(() => currentOverrides ? JSON.stringify(currentOverrides, null, 2) : placeholder);
|
||||
|
||||
const [readOnlyHeight, setReadOnlyHeight] = useState<number | undefined>(undefined);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// reset when dialog toggles
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const cur = settingsState.overridesOfModel?.[providerName]?.[modelName];
|
||||
setOverrideEnabled(!!cur);
|
||||
// If there are overrides, show them; otherwise use default values
|
||||
setJsonText(cur ? JSON.stringify(cur, null, 2) : placeholder);
|
||||
setErrorMsg(null);
|
||||
}, [isOpen, providerName, modelName, settingsState.overridesOfModel, placeholder]);
|
||||
|
||||
const onSave = async () => {
|
||||
|
||||
// if disabled override, reset overrides
|
||||
if (!overrideEnabled) {
|
||||
await settingsStateService.setOverridesOfModel(providerName, modelName, undefined);
|
||||
@@ -251,9 +255,10 @@ const SimpleModelSettingsDialog = ({
|
||||
// enabled overrides
|
||||
// parse json
|
||||
let parsedInput: Record<string, unknown>
|
||||
if (jsonText.trim()) {
|
||||
|
||||
if (textAreaRef.current?.value) {
|
||||
try {
|
||||
parsedInput = JSON.parse(jsonText);
|
||||
parsedInput = JSON.parse(textAreaRef.current.value);
|
||||
} catch (e) {
|
||||
setErrorMsg('Invalid JSON');
|
||||
return;
|
||||
@@ -265,10 +270,10 @@ const SimpleModelSettingsDialog = ({
|
||||
|
||||
// only keep allowed keys
|
||||
const cleaned: Partial<ModelOverrides> = {};
|
||||
for (const k of allowedKeys) {
|
||||
for (const k of modelOverrideKeys) {
|
||||
if (!(k in parsedInput)) continue
|
||||
const isEmpty = parsedInput[k] === '' || parsedInput[k] === null || parsedInput[k] === undefined;
|
||||
if (!isEmpty && (k in partialDefaults)) {
|
||||
if (!isEmpty) {
|
||||
cleaned[k] = parsedInput[k] as any;
|
||||
}
|
||||
}
|
||||
@@ -276,7 +281,7 @@ const SimpleModelSettingsDialog = ({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const sourcecodeOverridesLink = `https://github.com/voideditor/void/blob/d33b5ff9a32a748a22ac99e543a9eedd2e600062/src/vs/workbench/contrib/void/common/modelCapabilities.ts#L146-L170`
|
||||
const sourcecodeOverridesLink = `https://github.com/voideditor/void/blob/2e5ecb291d33afbe4565921664fb7e183189c1c5/src/vs/workbench/contrib/void/common/modelCapabilities.ts#L146-L172`
|
||||
|
||||
return (
|
||||
<div // Backdrop
|
||||
@@ -333,10 +338,11 @@ const SimpleModelSettingsDialog = ({
|
||||
</div>}
|
||||
|
||||
<textarea
|
||||
key={overrideEnabled + ''}
|
||||
ref={textAreaRef}
|
||||
className={`w-full min-h-[200px] p-2 rounded-sm border border-void-border-2 bg-void-bg-2 resize-none font-mono text-sm ${!overrideEnabled ? 'text-void-fg-3' : ''}`}
|
||||
value={overrideEnabled ? jsonText : placeholder}
|
||||
defaultValue={overrideEnabled && currentOverrides ? JSON.stringify(currentOverrides, null, 2) : placeholder}
|
||||
placeholder={placeholder}
|
||||
onChange={overrideEnabled ? (e) => setJsonText(e.target.value) : undefined}
|
||||
readOnly={!overrideEnabled}
|
||||
/>
|
||||
{errorMsg && (
|
||||
@@ -460,7 +466,7 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
|
||||
{/* left part is width:full */}
|
||||
<div className={`flex flex-grow items-center gap-4`}>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
|
||||
<span className='w-fit truncate'>{modelName}</span>
|
||||
<span className='w-fit max-w-[400px] truncate'>{modelName}</span>
|
||||
</div>
|
||||
|
||||
{/* right part is anything that fits */}
|
||||
@@ -499,7 +505,15 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
|
||||
|
||||
{/* X button */}
|
||||
<div className={`w-5 flex items-center justify-center`}>
|
||||
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName); }}><X className="size-4" /></button>}
|
||||
{type === 'default' || type === 'autodetected' ? null : <button
|
||||
onClick={() => { settingsStateService.deleteModel(providerName, modelName); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='right'
|
||||
data-tooltip-content='Delete'
|
||||
className={`${hasOverrides ? '' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}
|
||||
>
|
||||
<X size={12} className="text-void-fg-3 opacity-50" />
|
||||
</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -904,8 +918,130 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
|
||||
|
||||
// full settings
|
||||
|
||||
// MCP Server component
|
||||
const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer }) => {
|
||||
const accessor = useAccessor();
|
||||
const mcpService = accessor.get('IMCPService');
|
||||
|
||||
const voidSettings = useSettingsState()
|
||||
const isOn = voidSettings.mcpUserStateOfName[name]?.isOn
|
||||
|
||||
const removeUniquePrefix = (name: string) => name.split('_').slice(1).join('_')
|
||||
|
||||
return (
|
||||
<div className="border border-void-border-2 bg-void-bg-1 py-3 px-4 rounded-sm my-2">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left side - status and name */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Status indicator */}
|
||||
<div className={`w-2 h-2 rounded-full
|
||||
${server.status === 'success' ? 'bg-green-500'
|
||||
: server.status === 'error' ? 'bg-red-500'
|
||||
: server.status === 'loading' ? 'bg-yellow-500'
|
||||
: server.status === 'offline' ? 'bg-void-fg-3'
|
||||
: ''}
|
||||
`}></div>
|
||||
|
||||
{/* Server name */}
|
||||
<div className="text-sm font-medium text-void-fg-1">{name}</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - power toggle switch */}
|
||||
<VoidSwitch
|
||||
value={isOn ?? false}
|
||||
size='xs'
|
||||
disabled={server.status === 'error'}
|
||||
onChange={() => mcpService.toggleServerIsOn(name, !isOn)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tools section */}
|
||||
{isOn && (
|
||||
<div className="mt-3">
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
||||
{(server.tools ?? []).length > 0 ? (
|
||||
(server.tools ?? []).map((tool: { name: string; description?: string }) => (
|
||||
<span
|
||||
key={tool.name}
|
||||
className="px-2 py-0.5 bg-void-bg-2 text-void-fg-3 rounded-sm text-xs"
|
||||
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={tool.description || ''}
|
||||
data-tooltip-class-name='void-max-w-[300px]'
|
||||
>
|
||||
{removeUniquePrefix(tool.name)}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-void-fg-3">No tools available</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Command badge */}
|
||||
{isOn && server.command && (
|
||||
<div className="mt-3">
|
||||
<div className="text-xs text-void-fg-3 mb-1">Command:</div>
|
||||
<div className="px-2 py-1 bg-void-bg-2 text-xs font-mono overflow-x-auto whitespace-nowrap text-void-fg-2 rounded-sm">
|
||||
{server.command}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message if present */}
|
||||
{server.error && (
|
||||
<div className="mt-3">
|
||||
<WarningBox text={server.error} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main component that renders the list of servers
|
||||
const MCPServersList = () => {
|
||||
const mcpServiceState = useMCPServiceState()
|
||||
|
||||
let content: React.ReactNode
|
||||
if (mcpServiceState.error) {
|
||||
content = <div className="text-void-fg-3 text-sm mt-2">
|
||||
{mcpServiceState.error}
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
const entries = Object.entries(mcpServiceState.mcpServerOfName)
|
||||
if (entries.length === 0) {
|
||||
content = <div className="text-void-fg-3 text-sm mt-2">
|
||||
No servers found
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
content = entries.map(([name, server]) => (
|
||||
<MCPServerComponent key={name} name={name} server={server} />
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="my-2">{content}</div>
|
||||
};
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark()
|
||||
// ─── sidebar nav ──────────────────────────
|
||||
const [selectedSection, setSelectedSection] =
|
||||
useState<Tab>('models');
|
||||
|
||||
const navItems: { tab: Tab; label: string }[] = [
|
||||
{ tab: 'models', label: 'Models' },
|
||||
{ tab: 'localProviders', label: 'Local Providers' },
|
||||
{ tab: 'providers', label: 'Other Providers' },
|
||||
{ tab: 'featureOptions', label: 'Feature Options' },
|
||||
{ tab: 'general', label: 'General' },
|
||||
{ tab: 'mcp', label: 'MCP' },
|
||||
{ tab: 'all', label: 'All Settings' },
|
||||
];
|
||||
const shouldShowTab = (tab: Tab) => selectedSection === 'all' || selectedSection === tab;
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const environmentService = accessor.get('IEnvironmentService')
|
||||
@@ -914,6 +1050,7 @@ export const Settings = () => {
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const notificationService = accessor.get('INotificationService')
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
|
||||
const onDownload = (t: 'Chats' | 'Settings') => {
|
||||
let dataStr: string
|
||||
@@ -979,278 +1116,378 @@ export const Settings = () => {
|
||||
}
|
||||
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
return (
|
||||
<div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%', overflow: 'auto' }}>
|
||||
<div className="flex flex-col md:flex-row w-full gap-6 max-w-[900px] mx-auto mb-32" style={{ minHeight: '80vh' }}>
|
||||
{/* ────────────── SIDEBAR ────────────── */}
|
||||
|
||||
<div className='max-w-xl mx-auto'>
|
||||
<aside className="md:w-1/4 w-full p-6 shrink-0">
|
||||
{/* vertical tab list */}
|
||||
<div className="flex flex-col gap-2 mt-12">
|
||||
{navItems.map(({ tab, label }) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => {
|
||||
if (tab === 'all') {
|
||||
setSelectedSection('all');
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
setSelectedSection(tab);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
py-2 px-4 rounded-md text-left transition-all duration-200
|
||||
${selectedSection === tab
|
||||
? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm'
|
||||
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'}
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
|
||||
|
||||
<div className='w-full h-[1px] my-2' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<RedoOnboardingButton />
|
||||
</ErrorBoundary>
|
||||
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ModelDump />
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
<OllamaSetupInstructions sayWeAutoDetect={true} />
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
{/* ───────────── MAIN PANE ───────────── */}
|
||||
<main className="flex-1 p-6 select-none">
|
||||
|
||||
|
||||
|
||||
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
|
||||
<div className='max-w-3xl'>
|
||||
|
||||
<div className='flex flex-col gap-y-8 my-4'>
|
||||
<ErrorBoundary>
|
||||
{/* FIM */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>
|
||||
<span>
|
||||
Experimental.{' '}
|
||||
</span>
|
||||
<span
|
||||
className='hover:brightness-110'
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
|
||||
data-tooltip-class-name='void-max-w-[20px]'
|
||||
>
|
||||
Only works with FIM models.*
|
||||
</span>
|
||||
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
|
||||
|
||||
<div className='w-full h-[1px] my-2' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<RedoOnboardingButton />
|
||||
</ErrorBoundary>
|
||||
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
{/* All sections in flex container with gap-12 */}
|
||||
<div className='flex flex-col gap-12'>
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<div className={shouldShowTab('models') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ModelDump />
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Enable Switch */}
|
||||
{/* Local Providers section */}
|
||||
<div className={shouldShowTab('localProviders') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.enableAutocomplete}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
|
||||
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
<OllamaSetupInstructions sayWeAutoDetect={true} />
|
||||
</div>
|
||||
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Other Providers section */}
|
||||
<div className={shouldShowTab('providers') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Other Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Feature Options section */}
|
||||
<div className={shouldShowTab('featureOptions') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Feature Options</h2>
|
||||
|
||||
<div className='flex flex-col gap-y-8 my-4'>
|
||||
<ErrorBoundary>
|
||||
{/* FIM */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>
|
||||
<span>
|
||||
Experimental.{' '}
|
||||
</span>
|
||||
<span
|
||||
className='hover:brightness-110'
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
|
||||
data-tooltip-class-name='void-max-w-[20px]'
|
||||
>
|
||||
Only works with FIM models.*
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Enable Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.enableAutocomplete}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<ErrorBoundary>
|
||||
<div className={`my-2 ${!settingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Apply */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.syncApplyToChat}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${settingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Fast Apply Method Dropdown */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<FastApplyMethodDropdown />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Tools Section */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>Tools</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
{[...toolApprovalTypes].map((approvalType) => {
|
||||
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
</div>
|
||||
})}
|
||||
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Tool Lint Errors Switch */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.includeToolLintErrors}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Fix lint errors`}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<ErrorBoundary>
|
||||
<div className={`my-2 ${!settingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
{/* General section */}
|
||||
<div className={`${shouldShowTab('general') ? `` : 'hidden'} flex flex-col gap-12`}>
|
||||
{/* One-Click Switch section */}
|
||||
<div>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2'>One-Click Switch</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer your editor settings into Void.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Import/Export section */}
|
||||
<div>
|
||||
<h2 className='text-3xl mb-2'>Import/Export</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer Void's settings and chats in and out of Void.`}</h4>
|
||||
<div className='flex flex-col gap-8'>
|
||||
{/* Settings Subcategory */}
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
|
||||
Import Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
|
||||
Export Settings
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
|
||||
Reset Settings
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
{/* Chats Subcategory */}
|
||||
<div className='flex flex-col gap-2 w-full max-w-48'>
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Import Chats
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
|
||||
Export Chats
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
|
||||
Reset Chats
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Apply */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.syncApplyToChat}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${settingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
|
||||
|
||||
{/* Built-in Settings section */}
|
||||
<div>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<ErrorBoundary>
|
||||
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Fast Apply Method Dropdown */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<FastApplyMethodDropdown />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Tools Section */}
|
||||
<div>
|
||||
<h4 className={`text-base`}>Tools</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
{[...toolApprovalTypes].map((approvalType) => {
|
||||
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
</div>
|
||||
})}
|
||||
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Tool Lint Errors Switch */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.includeToolLintErrors}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Fix lint errors`}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* General section (formerly GeneralTab) */}
|
||||
<div className='mt-12'>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer your editor settings into Void.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Import/Export section, as its own block right after One-Click Switch */}
|
||||
<div className='mt-12'>
|
||||
<h2 className='text-3xl mb-2'>Import/Export</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer Void's settings and chats in and out of Void.`}</h4>
|
||||
<div className='flex flex-col gap-8'>
|
||||
{/* Settings Subcategory */}
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
|
||||
Import Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
|
||||
Export Settings
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
|
||||
Reset Settings
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
{/* Chats Subcategory */}
|
||||
<div className='flex flex-col gap-2 w-full max-w-48'>
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Import Chats
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
|
||||
Export Chats
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
|
||||
Reset Chats
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<ErrorBoundary>
|
||||
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
{/* AI Instructions section */}
|
||||
<div className='max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
System instructions to include with all AI requests.
|
||||
Alternatively, place a \`.voidrules\` file in the root of your workspace.
|
||||
`} chatMessageLocation={undefined} />
|
||||
</h4>
|
||||
<ErrorBoundary>
|
||||
<AIInstructionsBox />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</h4>
|
||||
<ErrorBoundary>
|
||||
<AIInstructionsBox />
|
||||
</ErrorBoundary>
|
||||
{/* --- Disable System Message Toggle --- */}
|
||||
<div className='my-4'>
|
||||
<ErrorBoundary>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={!!settingsState.globalSettings.disableSystemMessage}
|
||||
onChange={(newValue) => {
|
||||
voidSettingsService.setGlobalSetting('disableSystemMessage', newValue);
|
||||
}}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>
|
||||
{'Disable system message'}
|
||||
</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
<div className='text-void-fg-3 text-xs mt-1'>
|
||||
{`When disabled, Void will not include anything in the system message except for content you specified above.`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* MCP section */}
|
||||
<div className={shouldShowTab('mcp') ? `` : 'hidden'}>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2'>MCP</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
Use Model Context Protocol to provide Agent mode with more tools.
|
||||
`} chatMessageLocation={undefined} />
|
||||
</h4>
|
||||
<div className='my-2'>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full max-w-48' onClick={async () => { await mcpService.revealMCPConfigFile() }}>
|
||||
Add MCP Server
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<MCPServersList />
|
||||
</ErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ export const VoidTooltip = () => {
|
||||
padding: 0px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 999999;
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#void-tooltip {
|
||||
|
||||
@@ -89,10 +89,7 @@ registerAction2(class extends Action2 {
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
|
||||
// Get the views service to check if the sidebar is open
|
||||
// const viewsService = accessor.get(IViewsService)
|
||||
// Get services
|
||||
const commandService = accessor.get(ICommandService)
|
||||
const viewsService = accessor.get(IViewsService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
@@ -101,40 +98,47 @@ registerAction2(class extends Action2 {
|
||||
|
||||
metricsService.capture('Ctrl+L', {})
|
||||
|
||||
// capture selection and model before opening the chat panel
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const model = editor?.getModel()
|
||||
if (!model) return
|
||||
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
// open panel
|
||||
const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
|
||||
if (!wasAlreadyOpen) {
|
||||
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// if was already open
|
||||
|
||||
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
|
||||
if (!model) return
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
// if has no selection, close + return
|
||||
if (!selectionRange) {
|
||||
viewsService.closeViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
return;
|
||||
// Add selection to chat
|
||||
// add line selection
|
||||
if (selectionRange) {
|
||||
editor?.setSelection({
|
||||
startLineNumber: selectionRange.startLineNumber,
|
||||
endLineNumber: selectionRange.endLineNumber,
|
||||
startColumn: 1,
|
||||
endColumn: Number.MAX_SAFE_INTEGER
|
||||
})
|
||||
chatThreadService.addNewStagingSelection({
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
})
|
||||
}
|
||||
// add file
|
||||
else {
|
||||
chatThreadService.addNewStagingSelection({
|
||||
type: 'File',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
})
|
||||
}
|
||||
|
||||
// if has selection, add it
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
chatThreadService.addNewStagingSelection({
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
})
|
||||
|
||||
await chatThreadService.focusCurrentChat()
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
@@ -168,7 +172,7 @@ registerAction2(class extends Action2 {
|
||||
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
|
||||
|
||||
const oldSelns = oldThread?.state.stagingSelections
|
||||
const oldVal = oldUI?.textAreaRef.current?.value
|
||||
const oldVal = oldUI?.textAreaRef?.current?.value
|
||||
|
||||
// open and focus new thread
|
||||
chatThreadsService.openNewThread()
|
||||
|
||||
@@ -22,7 +22,12 @@ export interface ITerminalToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
listPersistentTerminalIds(): string[];
|
||||
runCommand(command: string, opts: { type: 'persistent', persistentTerminalId: string } | { type: 'ephemeral', cwd: string | null, terminalId: string }): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
|
||||
runCommand(command: string, opts:
|
||||
| { type: 'persistent', persistentTerminalId: string }
|
||||
| { type: 'temporary', cwd: string | null, terminalId: string }
|
||||
// | { type: 'apply', terminalId: string }
|
||||
): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
|
||||
|
||||
focusPersistentTerminal(terminalId: string): Promise<void>
|
||||
persistentTerminalExists(terminalId: string): boolean
|
||||
|
||||
@@ -277,6 +282,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
terminal.dispose()
|
||||
if (!isPersistent)
|
||||
delete this.temporaryTerminalInstanceOfId[params.terminalId]
|
||||
else
|
||||
delete this.persistentTerminalInstanceOfId[params.persistentTerminalId]
|
||||
}
|
||||
|
||||
const waitForResult = async () => {
|
||||
@@ -339,20 +346,20 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
await Promise.any([waitUntilDone, waitUntilInterrupt])
|
||||
.finally(() => disposables.forEach(d => d.dispose()))
|
||||
|
||||
|
||||
|
||||
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
|
||||
if (resolveReason?.type === 'timeout') {
|
||||
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
|
||||
result = await this.readTerminal(terminalId)
|
||||
}
|
||||
|
||||
if (!isPersistent) {
|
||||
interrupt()
|
||||
}
|
||||
|
||||
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
||||
|
||||
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
|
||||
if (resolveReason.type === 'timeout') {
|
||||
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
|
||||
result = await this.readTerminal(terminalId)
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!isPersistent) result = `$ ${command}\n${result}`
|
||||
result = removeAnsiEscapeCodes(result)
|
||||
// trim
|
||||
|
||||
@@ -8,7 +8,7 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
|
||||
import { ISearchService } from '../../../services/search/common/search.js'
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js'
|
||||
import { ITerminalToolService } from './terminalToolService.js'
|
||||
import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { LintErrorItem, BuiltinToolCallParams, BuiltinToolResultType, BuiltinToolName } from '../common/toolsServiceTypes.js'
|
||||
import { IVoidModelService } from '../common/voidModelService.js'
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
||||
@@ -16,20 +16,15 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree
|
||||
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
|
||||
import { timeout } from '../../../../base/common/async.js'
|
||||
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js'
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
import { generateUuid } from '../../../../base/common/uuid.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
|
||||
|
||||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
||||
|
||||
type ValidateBuiltinParams = { [T in BuiltinToolName]: (p: RawToolParamsObj) => BuiltinToolCallParams[T] }
|
||||
type CallBuiltinTool = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T]) => Promise<{ result: BuiltinToolResultType[T] | Promise<BuiltinToolResultType[T]>, interruptTool?: () => void }> }
|
||||
type BuiltinToolResultToString = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T], result: Awaited<BuiltinToolResultType[T]>) => string }
|
||||
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
@@ -110,9 +105,9 @@ const checkIfIsFolder = (uriStr: string) => {
|
||||
|
||||
export interface IToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
validateParams: ValidateParams;
|
||||
callTool: CallTool;
|
||||
stringOfResult: ToolResultToString;
|
||||
validateParams: ValidateBuiltinParams;
|
||||
callTool: CallBuiltinTool;
|
||||
stringOfResult: BuiltinToolResultToString;
|
||||
}
|
||||
|
||||
export const IToolsService = createDecorator<IToolsService>('ToolsService');
|
||||
@@ -121,9 +116,9 @@ export class ToolsService implements IToolsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
public validateParams: ValidateParams;
|
||||
public callTool: CallTool;
|
||||
public stringOfResult: ToolResultToString;
|
||||
public validateParams: ValidateBuiltinParams;
|
||||
public callTool: CallBuiltinTool;
|
||||
public stringOfResult: BuiltinToolResultToString;
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@@ -430,7 +425,7 @@ export class ToolsService implements IToolsService {
|
||||
},
|
||||
// ---
|
||||
run_command: async ({ command, cwd, terminalId }) => {
|
||||
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'ephemeral', cwd, terminalId })
|
||||
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'temporary', cwd, terminalId })
|
||||
return { result: resPromise, interruptTool: interrupt }
|
||||
},
|
||||
run_persistent_command: async ({ command, persistentTerminalId }) => {
|
||||
@@ -446,7 +441,6 @@ export class ToolsService implements IToolsService {
|
||||
await this.terminalToolService.killPersistentTerminal(persistentTerminalId)
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -550,7 +544,6 @@ export class ToolsService implements IToolsService {
|
||||
kill_persistent_terminal: (params, _result) => {
|
||||
return `Successfully closed terminal "${params.persistentTerminalId}".`;
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ import './voidOnboardingService.js'
|
||||
// register misc service
|
||||
import './miscWokrbenchContrib.js'
|
||||
|
||||
// register file service (for explorer context menu)
|
||||
import './fileService.js'
|
||||
|
||||
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
|
||||
|
||||
// llmMessage
|
||||
|
||||
@@ -8,7 +8,7 @@ import Severity from '../../../../base/common/severity.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationActions, INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { INotificationActions, INotificationHandle, INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
@@ -20,7 +20,7 @@ import { IAction } from '../../../../base/common/actions.js';
|
||||
|
||||
|
||||
|
||||
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService) => {
|
||||
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService): INotificationHandle => {
|
||||
const message = res?.message || 'This is a very old version of Void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
|
||||
|
||||
let actions: INotificationActions | undefined
|
||||
@@ -119,18 +119,21 @@ const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifSe
|
||||
progress: actions ? { worked: 0, total: 100 } : undefined,
|
||||
actions: actions,
|
||||
})
|
||||
|
||||
return notifController
|
||||
// const d = notifController.onDidClose(() => {
|
||||
// notifyYesUpdate(notifService, res)
|
||||
// d.dispose()
|
||||
// })
|
||||
}
|
||||
const notifyErrChecking = (notifService: INotificationService) => {
|
||||
const notifyErrChecking = (notifService: INotificationService): INotificationHandle => {
|
||||
const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!`
|
||||
notifService.notify({
|
||||
const notifController = notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: message,
|
||||
sticky: true,
|
||||
})
|
||||
return notifController
|
||||
}
|
||||
|
||||
|
||||
@@ -140,30 +143,35 @@ const performVoidCheck = async (
|
||||
voidUpdateService: IVoidUpdateService,
|
||||
metricsService: IMetricsService,
|
||||
updateService: IUpdateService,
|
||||
) => {
|
||||
): Promise<INotificationHandle | null> => {
|
||||
|
||||
const metricsTag = explicit ? 'Manual' : 'Auto'
|
||||
|
||||
metricsService.capture(`Void Update ${metricsTag}: Checking...`, {})
|
||||
const res = await voidUpdateService.check(explicit)
|
||||
if (!res) {
|
||||
notifyErrChecking(notifService);
|
||||
const notifController = notifyErrChecking(notifService);
|
||||
metricsService.capture(`Void Update ${metricsTag}: Error`, { res })
|
||||
return notifController
|
||||
}
|
||||
else {
|
||||
if (res.message) {
|
||||
notifyUpdate(res, notifService, updateService)
|
||||
const notifController = notifyUpdate(res, notifService, updateService)
|
||||
metricsService.capture(`Void Update ${metricsTag}: Yes`, { res })
|
||||
return notifController
|
||||
}
|
||||
else {
|
||||
metricsService.capture(`Void Update ${metricsTag}: No`, { res })
|
||||
return
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Action
|
||||
let lastNotifController: INotificationHandle | null = null
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -177,7 +185,15 @@ registerAction2(class extends Action2 {
|
||||
const notifService = accessor.get(INotificationService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const updateService = accessor.get(IUpdateService)
|
||||
performVoidCheck(true, notifService, voidUpdateService, metricsService, updateService)
|
||||
|
||||
const currNotifController = lastNotifController
|
||||
|
||||
const newController = await performVoidCheck(true, notifService, voidUpdateService, metricsService, updateService)
|
||||
|
||||
if (newController) {
|
||||
currNotifController?.close()
|
||||
lastNotifController = newController
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -5,31 +5,32 @@
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
import { ToolCallParams, ToolName, ToolResult } from './toolsServiceTypes.js';
|
||||
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
content: string; // give this result to LLM (string of value)
|
||||
id: string;
|
||||
rawParams: RawToolParamsObj;
|
||||
mcpServerName: string | undefined; // the server name at the time of the call
|
||||
} & (
|
||||
// in order of events:
|
||||
| { type: 'invalid_params', result: null, name: T, }
|
||||
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams<T>, } // params were validated, awaiting user
|
||||
|
||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
|
||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams<T>, }
|
||||
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
|
||||
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T] }
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams<T>, } // error when tool was running
|
||||
| { type: 'success', result: Awaited<ToolResult<T>>, name: T, params: ToolCallParams<T>, }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams<T> }
|
||||
) // user rejected
|
||||
|
||||
export type DecorativeCanceledTool = {
|
||||
role: 'interrupted_streaming_tool';
|
||||
name: ToolName;
|
||||
mcpServerName: string | undefined; // the server name at the time of the call
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
import { ShallowDirectoryItem, BuiltinToolCallParams, BuiltinToolResultType } from './toolsServiceTypes.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export const computeDirectoryTree1Deep = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1,
|
||||
): Promise<ToolResultType['ls_dir']> => {
|
||||
): Promise<BuiltinToolResultType['ls_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
@@ -107,7 +107,7 @@ export const computeDirectoryTree1Deep = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
||||
export const stringifyDirectoryTree1Deep = (params: BuiltinToolCallParams['ls_dir'], result: BuiltinToolResultType['ls_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.uri} is not a directory`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IPathService } from '../../../services/path/common/pathService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { MCPServerOfName, MCPConfigFileJSON, MCPServer, MCPToolCallParams, RawMCPToolCall, MCPServerEventResponse } from './mcpServiceTypes.js';
|
||||
import { Event, Emitter } from '../../../../base/common/event.js';
|
||||
import { InternalToolInfo } from './prompt/prompts.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { MCPUserStateOfName } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
type MCPServiceState = {
|
||||
mcpServerOfName: MCPServerOfName,
|
||||
error: string | undefined, // global parsing error
|
||||
}
|
||||
|
||||
export interface IMCPService {
|
||||
readonly _serviceBrand: undefined;
|
||||
revealMCPConfigFile(): Promise<void>;
|
||||
toggleServerIsOn(serverName: string, isOn: boolean): Promise<void>;
|
||||
|
||||
readonly state: MCPServiceState; // NOT persisted
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
getMCPTools(): InternalToolInfo[] | undefined;
|
||||
callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }>;
|
||||
stringifyResult(result: RawMCPToolCall): string
|
||||
}
|
||||
|
||||
export const IMCPService = createDecorator<IMCPService>('mcpConfigService');
|
||||
|
||||
|
||||
|
||||
const MCP_CONFIG_FILE_NAME = 'mcp.json';
|
||||
const MCP_CONFIG_SAMPLE = { mcpServers: {} }
|
||||
const MCP_CONFIG_SAMPLE_STRING = JSON.stringify(MCP_CONFIG_SAMPLE, null, 2);
|
||||
|
||||
|
||||
// export interface MCPCallToolOfToolName {
|
||||
// [toolName: string]: (params: any) => Promise<{
|
||||
// result: any | Promise<any>,
|
||||
// interruptTool?: () => void
|
||||
// }>;
|
||||
// }
|
||||
|
||||
|
||||
class MCPService extends Disposable implements IMCPService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
|
||||
private readonly channel: IChannel // MCPChannel
|
||||
|
||||
// list of MCP servers pulled from mcpChannel
|
||||
state: MCPServiceState = {
|
||||
mcpServerOfName: {},
|
||||
error: undefined,
|
||||
}
|
||||
|
||||
// Emitters for server events
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
public readonly onDidChangeState = this._onDidChangeState.event;
|
||||
|
||||
// private readonly _onLoadingServersChange = new Emitter<MCPServerEventLoadingParam>();
|
||||
// public readonly onLoadingServersChange = this._onLoadingServersChange.event;
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IPathService private readonly pathService: IPathService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
super();
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-mcp')
|
||||
|
||||
|
||||
const onEvent = (e: MCPServerEventResponse) => {
|
||||
console.log('GOT EVENT', e)
|
||||
this._setMCPServerState(e.response.name, e.response.newServer)
|
||||
}
|
||||
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
|
||||
private async _initialize() {
|
||||
try {
|
||||
await this.voidSettingsService.waitForInitState;
|
||||
|
||||
// Create .mcpConfig if it doesn't exist
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
const fileExists = await this._configFileExists(mcpConfigUri);
|
||||
if (!fileExists) {
|
||||
await this._createMCPConfigFile(mcpConfigUri);
|
||||
console.log('MCP Config file created:', mcpConfigUri.toString());
|
||||
}
|
||||
await this._addMCPConfigFileWatcher();
|
||||
await this._refreshMCPServers();
|
||||
} catch (error) {
|
||||
console.error('Error initializing MCPService:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _setMCPServerState = async (serverName: string, newServer: MCPServer | undefined) => {
|
||||
if (newServer === undefined) {
|
||||
// Remove the server from the state
|
||||
const { [serverName]: removed, ...remainingServers } = this.state.mcpServerOfName;
|
||||
this.state = {
|
||||
...this.state,
|
||||
mcpServerOfName: remainingServers
|
||||
}
|
||||
} else {
|
||||
// Add or update the server
|
||||
this.state = {
|
||||
...this.state,
|
||||
mcpServerOfName: {
|
||||
...this.state.mcpServerOfName,
|
||||
[serverName]: newServer
|
||||
}
|
||||
}
|
||||
}
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
private readonly _setHasError = async (errMsg: string | undefined) => {
|
||||
this.state = {
|
||||
...this.state,
|
||||
error: errMsg,
|
||||
}
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
// Create the file/directory if it doesn't exist
|
||||
private async _createMCPConfigFile(mcpConfigUri: URI): Promise<void> {
|
||||
await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path }));
|
||||
const buffer = VSBuffer.fromString(MCP_CONFIG_SAMPLE_STRING);
|
||||
await this.fileService.writeFile(mcpConfigUri, buffer);
|
||||
}
|
||||
|
||||
|
||||
private async _addMCPConfigFileWatcher(): Promise<void> {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
this._register(
|
||||
this.fileService.watch(mcpConfigUri)
|
||||
)
|
||||
|
||||
this._register(this.fileService.onDidFilesChange(async e => {
|
||||
if (!e.contains(mcpConfigUri)) return
|
||||
await this._refreshMCPServers();
|
||||
}));
|
||||
}
|
||||
|
||||
// Client-side functions
|
||||
|
||||
public async revealMCPConfigFile(): Promise<void> {
|
||||
try {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
await this.editorService.openEditor({
|
||||
resource: mcpConfigUri,
|
||||
options: {
|
||||
pinned: true,
|
||||
revealIfOpened: true,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error opening MCP config file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public getMCPTools(): InternalToolInfo[] | undefined {
|
||||
const allTools: InternalToolInfo[] = []
|
||||
for (const serverName in this.state.mcpServerOfName) {
|
||||
const server = this.state.mcpServerOfName[serverName];
|
||||
server.tools?.forEach(tool => {
|
||||
allTools.push({
|
||||
description: tool.description || '',
|
||||
params: this._transformInputSchemaToParams(tool.inputSchema),
|
||||
name: tool.name,
|
||||
mcpServerName: serverName,
|
||||
})
|
||||
})
|
||||
}
|
||||
if (allTools.length === 0) return undefined
|
||||
return allTools
|
||||
}
|
||||
|
||||
private _transformInputSchemaToParams(inputSchema?: Record<string, any>): { [paramName: string]: { description: string } } {
|
||||
|
||||
// Check if inputSchema is valid
|
||||
if (!inputSchema || !inputSchema.properties) return {};
|
||||
|
||||
const params: { [paramName: string]: { description: string } } = {};
|
||||
Object.keys(inputSchema.properties).forEach(paramName => {
|
||||
const propertyValues = inputSchema.properties[paramName];
|
||||
|
||||
// Check if propertyValues is not an object
|
||||
if (typeof propertyValues !== 'object') {
|
||||
console.warn(`Invalid property value for ${paramName}: expected object, got ${typeof propertyValues}`);
|
||||
return; // in forEach the return is equivalent to continue
|
||||
}
|
||||
|
||||
// Add the parameter to the params object
|
||||
params[paramName] = {
|
||||
description: JSON.stringify(propertyValues.description || '', null, 2) || '',
|
||||
}
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
private async _getMCPConfigFilePath(): Promise<URI> {
|
||||
const appName = this.productService.dataFolderName
|
||||
const userHome = await this.pathService.userHome();
|
||||
const uri = URI.joinPath(userHome, appName, MCP_CONFIG_FILE_NAME)
|
||||
return uri
|
||||
}
|
||||
|
||||
private async _configFileExists(mcpConfigUri: URI): Promise<boolean> {
|
||||
try {
|
||||
await this.fileService.stat(mcpConfigUri);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async _parseMCPConfigFile(): Promise<MCPConfigFileJSON | null> {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
try {
|
||||
const fileContent = await this.fileService.readFile(mcpConfigUri);
|
||||
const contentString = fileContent.value.toString();
|
||||
const configFileJson = JSON.parse(contentString);
|
||||
if (!configFileJson.mcpServers) {
|
||||
throw new Error('Missing mcpServers property');
|
||||
}
|
||||
return configFileJson as MCPConfigFileJSON;
|
||||
} catch (error) {
|
||||
const fullError = `Error parsing MCP config file: ${error}`;
|
||||
this._setHasError(fullError)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle server state changes
|
||||
private async _refreshMCPServers(): Promise<void> {
|
||||
|
||||
this._setHasError(undefined)
|
||||
|
||||
const newConfigFileJSON = await this._parseMCPConfigFile();
|
||||
if (!newConfigFileJSON) { console.log(`Not setting state: MCP config file not found`); return }
|
||||
if (!newConfigFileJSON?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return }
|
||||
|
||||
|
||||
const oldConfigFileNames = Object.keys(this.state.mcpServerOfName)
|
||||
const newConfigFileNames = Object.keys(newConfigFileJSON.mcpServers)
|
||||
|
||||
const addedServerNames = newConfigFileNames.filter(serverName => !oldConfigFileNames.includes(serverName)); // in new and not in old
|
||||
const removedServerNames = oldConfigFileNames.filter(serverName => !newConfigFileNames.includes(serverName)); // in old and not in new
|
||||
|
||||
// set isOn to any new servers in the config
|
||||
const addedUserStateOfName: MCPUserStateOfName = {}
|
||||
for (const name of addedServerNames) { addedUserStateOfName[name] = { isOn: true } }
|
||||
await this.voidSettingsService.addMCPUserStateOfNames(addedUserStateOfName);
|
||||
|
||||
// delete isOn for any servers that no longer show up in the config
|
||||
await this.voidSettingsService.removeMCPUserStateOfNames(removedServerNames);
|
||||
|
||||
// set all servers to loading
|
||||
for (const serverName in newConfigFileJSON.mcpServers) {
|
||||
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
|
||||
}
|
||||
const updatedServerNames = Object.keys(newConfigFileJSON.mcpServers).filter(serverName => !addedServerNames.includes(serverName) && !removedServerNames.includes(serverName))
|
||||
|
||||
this.channel.call('refreshMCPServers', {
|
||||
mcpConfigFileJSON: newConfigFileJSON,
|
||||
addedServerNames,
|
||||
removedServerNames,
|
||||
updatedServerNames,
|
||||
userStateOfName: this.voidSettingsService.state.mcpUserStateOfName,
|
||||
})
|
||||
}
|
||||
|
||||
stringifyResult(result: RawMCPToolCall): string {
|
||||
let toolResultStr: string
|
||||
if (result.event === 'text') {
|
||||
toolResultStr = result.text
|
||||
} else if (result.event === 'image') {
|
||||
toolResultStr = `[Image: ${result.image.mimeType}]`
|
||||
} else if (result.event === 'audio') {
|
||||
toolResultStr = `[Audio content]`
|
||||
} else if (result.event === 'resource') {
|
||||
toolResultStr = `[Resource content]`
|
||||
} else {
|
||||
toolResultStr = JSON.stringify(result)
|
||||
}
|
||||
return toolResultStr
|
||||
}
|
||||
|
||||
// toggle MCP server and update isOn in void settings
|
||||
public async toggleServerIsOn(serverName: string, isOn: boolean): Promise<void> {
|
||||
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
|
||||
|
||||
await this.voidSettingsService.setMCPServerState(serverName, { isOn });
|
||||
this.channel.call('toggleMCPServer', { serverName, isOn })
|
||||
}
|
||||
|
||||
|
||||
public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> {
|
||||
const result = await this.channel.call<RawMCPToolCall>('callTool', toolData);
|
||||
if (result.event === 'error') {
|
||||
throw new Error(`Error: ${result.text}`)
|
||||
}
|
||||
return { result };
|
||||
}
|
||||
|
||||
// public getMCPToolFns(): MCPToolResultType {
|
||||
// const tools = this.getMCPTools();
|
||||
// const toolFns: MCPToolResultType = {};
|
||||
|
||||
// tools.forEach((tool) => {
|
||||
// const name = tool.name;
|
||||
// // Define the tool call function
|
||||
// const toolFn = async (params: {
|
||||
// serverName: string,
|
||||
// toolName: string,
|
||||
// args: any
|
||||
// }) => {
|
||||
// const { serverName, toolName, args } = params;
|
||||
// const response = await this.callMCPTool({
|
||||
// serverName,
|
||||
// toolName,
|
||||
// params: args,
|
||||
// });
|
||||
// return { result: response }
|
||||
// };
|
||||
// toolFns[name] = toolFn;
|
||||
// });
|
||||
|
||||
// return toolFns
|
||||
// }
|
||||
}
|
||||
|
||||
registerSingleton(IMCPService, MCPService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* mcp-response-types.ts
|
||||
* --------------------------------------------------
|
||||
* **Pure** TypeScript interfaces (no external imports)
|
||||
* describing the JSON-RPC response shapes for:
|
||||
*
|
||||
* 1. tools/list -> ToolsListResponse
|
||||
* 2. prompts/list -> PromptsListResponse
|
||||
* 3. tools/call -> ToolCallResponse
|
||||
*
|
||||
* They are distilled directly from the official MCP
|
||||
* 2025‑03‑26 specification:
|
||||
* • Tools list response examples
|
||||
* • Prompts list response examples
|
||||
* • Tool call response examples
|
||||
*
|
||||
* Use them to get full IntelliSense when working with
|
||||
* @modelcontextprotocol/inspector‑cli responses.
|
||||
*/
|
||||
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Core JSON‑RPC envelope */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface JsonRpcSuccess<T> {
|
||||
// /** JSON‑RPC version – always '2.0' */
|
||||
// jsonrpc: '2.0';
|
||||
// /** Request identifier echoed back by the server */
|
||||
// id: string | number | null;
|
||||
// /** The successful result payload */
|
||||
// result: T;
|
||||
// }
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Utility: pagination */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface Paginated {
|
||||
// /** Opaque cursor for fetching the next page */
|
||||
// nextCursor?: string;
|
||||
// }
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 1. tools/list */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface MCPTool {
|
||||
/** Unique tool identifier */
|
||||
name: string;
|
||||
/** Human‑readable description */
|
||||
description?: string;
|
||||
/** JSON schema describing expected arguments */
|
||||
inputSchema?: Record<string, unknown>;
|
||||
/** Free‑form annotations describing behaviour, security, etc. */
|
||||
annotations?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// export interface ToolsListResult extends Paginated {
|
||||
// tools: MCPTool[];
|
||||
// }
|
||||
|
||||
// export type ToolsListResponse = JsonRpcSuccess<ToolsListResult>;
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 2. prompts/list */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface PromptArgument {
|
||||
// name: string;
|
||||
// description?: string;
|
||||
// /** Whether the argument is required */
|
||||
// required?: boolean;
|
||||
// }
|
||||
|
||||
// export interface Prompt {
|
||||
// name: string;
|
||||
// description?: string;
|
||||
// arguments?: PromptArgument[];
|
||||
// }
|
||||
|
||||
// export interface PromptsListResult extends Paginated {
|
||||
// prompts: Prompt[];
|
||||
// }
|
||||
|
||||
// export type PromptsListResponse = JsonRpcSuccess<PromptsListResult>;
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 3. tools/call */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
/** Additional resource structure that can be embedded in tool results */
|
||||
// export interface Resource {
|
||||
// uri: string;
|
||||
// mimeType: string;
|
||||
// /** Either plain‑text or base64‑encoded binary data */
|
||||
// text?: string;
|
||||
// data?: string;
|
||||
// }
|
||||
|
||||
/** Individual content items returned by a tool */
|
||||
// export type ToolContent =
|
||||
// | { type: 'text'; text: string }
|
||||
// | { type: 'image'; data: string; mimeType: string }
|
||||
// | { type: 'audio'; data: string; mimeType: string }
|
||||
// | { type: 'resource'; resource: Resource };
|
||||
|
||||
// export interface ToolCallResult {
|
||||
// /** List of content parts (text, images, resources, etc.) */
|
||||
// content: ToolContent[];
|
||||
// /** True if the tool itself encountered a domain‑level error */
|
||||
// isError?: boolean;
|
||||
// }
|
||||
|
||||
// export type ToolCallResponse = JsonRpcSuccess<ToolCallResult>;
|
||||
|
||||
// MCP SERVER CONFIG FILE TYPES -----------------------------
|
||||
|
||||
export interface MCPConfigFileEntryJSON {
|
||||
// Command-based server properties
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
|
||||
// URL-based server properties
|
||||
url?: URL;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface MCPConfigFileJSON {
|
||||
mcpServers: Record<string, MCPConfigFileEntryJSON>;
|
||||
}
|
||||
|
||||
|
||||
// SERVER EVENT TYPES ------------------------------------------
|
||||
|
||||
export type MCPServer = {
|
||||
// Command-based server properties
|
||||
tools: MCPTool[],
|
||||
status: 'loading' | 'success' | 'offline',
|
||||
command?: string,
|
||||
error?: string,
|
||||
} | {
|
||||
tools?: undefined,
|
||||
status: 'error',
|
||||
command?: string,
|
||||
error: string,
|
||||
}
|
||||
|
||||
export interface MCPServerOfName {
|
||||
[serverName: string]: MCPServer;
|
||||
}
|
||||
|
||||
export type MCPServerEvent = {
|
||||
name: string;
|
||||
prevServer?: MCPServer;
|
||||
newServer?: MCPServer;
|
||||
}
|
||||
export type MCPServerEventResponse = { response: MCPServerEvent }
|
||||
|
||||
export interface MCPConfigFileParseErrorResponse {
|
||||
response: {
|
||||
type: 'config-file-error';
|
||||
error: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// export type MCPServerResponse = MCPAddResponse | MCPUpdateResponse | MCPDeleteResponse | MCPLoadingResponse;
|
||||
|
||||
// Event parameter types
|
||||
// export type MCPServerEventAddParam = { response: MCPAddResponse };
|
||||
// export type MCPServerEventUpdateParam = { response: MCPUpdateResponse };
|
||||
// export type MCPServerEventDeleteParam = { response: MCPDeleteResponse };
|
||||
// export type MCPServerEventLoadingParam = { response: MCPLoadingResponse };
|
||||
|
||||
// Event Param union type
|
||||
// export type MCPServerEventParam = MCPServerEventAddParam | MCPServerEventUpdateParam | MCPServerEventDeleteParam | MCPServerEventLoadingParam;
|
||||
|
||||
// TOOL CALL EVENT TYPES ------------------------------------------
|
||||
|
||||
type MCPToolResponseType = 'text' | 'image' | 'audio' | 'resource' | 'error';
|
||||
|
||||
type ResponseImageTypes = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp' | 'image/svg+xml' | 'image/bmp' | 'image/tiff' | 'image/vnd.microsoft.icon';
|
||||
|
||||
interface ImageData {
|
||||
data: string;
|
||||
mimeType: ResponseImageTypes;
|
||||
}
|
||||
|
||||
interface MCPToolResponseBase {
|
||||
toolName: string;
|
||||
serverName?: string;
|
||||
event: MCPToolResponseType;
|
||||
text?: string;
|
||||
image?: ImageData;
|
||||
}
|
||||
|
||||
type MCPToolResponseConstraints = {
|
||||
'text': {
|
||||
image?: never;
|
||||
text: string;
|
||||
};
|
||||
'error': {
|
||||
image?: never;
|
||||
text: string;
|
||||
};
|
||||
'image': {
|
||||
text?: never;
|
||||
image: ImageData;
|
||||
};
|
||||
'audio': {
|
||||
text?: never;
|
||||
image?: never;
|
||||
};
|
||||
'resource': {
|
||||
text?: never;
|
||||
image?: never;
|
||||
}
|
||||
}
|
||||
|
||||
type MCPToolEventResponse<T extends MCPToolResponseType> = Omit<MCPToolResponseBase, 'event' | keyof MCPToolResponseConstraints> & MCPToolResponseConstraints[T] & { event: T };
|
||||
|
||||
// Response types
|
||||
export type MCPToolTextResponse = MCPToolEventResponse<'text'>;
|
||||
export type MCPToolErrorResponse = MCPToolEventResponse<'error'>;
|
||||
export type MCPToolImageResponse = MCPToolEventResponse<'image'>;
|
||||
export type MCPToolAudioResponse = MCPToolEventResponse<'audio'>;
|
||||
export type MCPToolResourceResponse = MCPToolEventResponse<'resource'>;
|
||||
export type RawMCPToolCall = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse;
|
||||
|
||||
export interface MCPToolCallParams {
|
||||
serverName: string;
|
||||
toolName: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
@@ -60,6 +60,12 @@ export const defaultProviderSettings = {
|
||||
apiKey: '',
|
||||
azureApiVersion: '2024-05-01-preview',
|
||||
},
|
||||
awsBedrock: {
|
||||
apiKey: '',
|
||||
region: 'us-east-1', // add region setting
|
||||
endpoint: '', // optionally allow overriding default
|
||||
},
|
||||
|
||||
} as const
|
||||
|
||||
|
||||
@@ -78,14 +84,19 @@ export const defaultModelsOfProvider = {
|
||||
// 'gpt-4o-mini',
|
||||
],
|
||||
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
'claude-opus-4-0',
|
||||
'claude-sonnet-4-0',
|
||||
'claude-3-7-sonnet-latest',
|
||||
'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',
|
||||
'grok-2',
|
||||
'grok-3',
|
||||
'grok-3-mini',
|
||||
'grok-3-fast',
|
||||
'grok-3-mini-fast'
|
||||
],
|
||||
gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
'gemini-2.5-pro-exp-03-25',
|
||||
@@ -106,11 +117,14 @@ export const defaultModelsOfProvider = {
|
||||
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
// 'anthropic/claude-3.7-sonnet:thinking',
|
||||
'anthropic/claude-opus-4',
|
||||
'anthropic/claude-sonnet-4',
|
||||
'qwen/qwen3-235b-a22b',
|
||||
'anthropic/claude-3.7-sonnet',
|
||||
'anthropic/claude-3.5-sonnet',
|
||||
'deepseek/deepseek-r1',
|
||||
'deepseek/deepseek-r1-zero:free',
|
||||
'mistralai/devstral-small:free'
|
||||
// 'openrouter/quasar-alpha',
|
||||
// 'google/gemini-2.5-pro-preview-03-25',
|
||||
// 'mistralai/codestral-2501',
|
||||
@@ -128,13 +142,16 @@ export const defaultModelsOfProvider = {
|
||||
],
|
||||
mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
'codestral-latest',
|
||||
'devstral-small-latest',
|
||||
'mistral-large-latest',
|
||||
'mistral-medium-latest',
|
||||
'ministral-3b-latest',
|
||||
'ministral-8b-latest',
|
||||
],
|
||||
openAICompatible: [], // fallback
|
||||
googleVertex: [],
|
||||
microsoftAzure: [],
|
||||
awsBedrock: [],
|
||||
liteLLM: [],
|
||||
|
||||
|
||||
@@ -153,6 +170,8 @@ export type VoidStaticModelInfo = { // not stateful
|
||||
specialToolFormat?: 'openai-style' | 'anthropic-style' | 'gemini-style', // typically you should use 'openai-style'. null means "can't call tools by default", and asks the LLM to output XML in agent mode
|
||||
supportsFIM: boolean; // whether the model was specifically designed for autocomplete or "FIM" ("fill-in-middle" format)
|
||||
|
||||
additionalOpenAIPayload?: { [key: string]: string } // additional payload in the message body for requests that are openai-compatible (ollama, vllm, openai, openrouter, etc)
|
||||
|
||||
// reasoning options
|
||||
reasoningCapabilities: false | {
|
||||
readonly supportsReasoning: true; // for clarity, this must be true if anything below is specified
|
||||
@@ -185,8 +204,20 @@ export type VoidStaticModelInfo = { // not stateful
|
||||
// if you change the above type, remember to update the Settings link
|
||||
|
||||
|
||||
export type ModelOverrides = Pick<VoidStaticModelInfo,
|
||||
'contextWindow' | 'reservedOutputTokenSpace' | 'specialToolFormat' | 'supportsSystemMessage' | 'supportsFIM' | 'reasoningCapabilities'
|
||||
|
||||
export const modelOverrideKeys = [
|
||||
'contextWindow',
|
||||
'reservedOutputTokenSpace',
|
||||
'supportsSystemMessage',
|
||||
'specialToolFormat',
|
||||
'supportsFIM',
|
||||
'reasoningCapabilities',
|
||||
'additionalOpenAIPayload'
|
||||
] as const
|
||||
|
||||
export type ModelOverrides = Pick<
|
||||
VoidStaticModelInfo,
|
||||
(typeof modelOverrideKeys)[number]
|
||||
>
|
||||
|
||||
|
||||
@@ -248,6 +279,12 @@ const openSourceModelOptions_assumingOAICompat = {
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, reservedOutputTokenSpace: 4_096,
|
||||
},
|
||||
'devstral': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 131_000, reservedOutputTokenSpace: 8_192,
|
||||
},
|
||||
'openhands-lm-32b': { // https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
@@ -362,22 +399,28 @@ const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallbac
|
||||
: VoidStaticModelInfo & { modelName: string, recognizedModelName: string } => {
|
||||
|
||||
const opts = obj[recognizedModelName]
|
||||
const supportsSystemMessage = opts.supportsSystemMessage === 'separated'
|
||||
? 'system-role'
|
||||
: opts.supportsSystemMessage
|
||||
|
||||
return {
|
||||
recognizedModelName,
|
||||
modelName,
|
||||
...opts,
|
||||
supportsSystemMessage: opts.supportsSystemMessage ? 'system-role' : false,
|
||||
supportsSystemMessage: supportsSystemMessage,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
...fallbackKnownValues
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions, 'gemini-2.5-pro-exp-03-25')
|
||||
|
||||
if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions, 'claude-3-5-sonnet-20241022')
|
||||
if (lower.includes('claude')) return toFallback(anthropicModelOptions, 'claude-3-7-sonnet-20250219')
|
||||
|
||||
if (lower.includes('grok')) return toFallback(xAIModelOptions, 'grok-2')
|
||||
if (lower.includes('grok2') || lower.includes('grok2')) return toFallback(xAIModelOptions, 'grok-2')
|
||||
if (lower.includes('grok')) return toFallback(xAIModelOptions, 'grok-3')
|
||||
|
||||
if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return toFallback(openSourceModelOptions_assumingOAICompat, 'deepseekR1')
|
||||
if (lower.includes('deepseek') && lower.includes('v2')) return toFallback(openSourceModelOptions_assumingOAICompat, 'deepseekCoderV2')
|
||||
@@ -397,6 +440,7 @@ const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallbac
|
||||
if (lower.includes('qwq')) { return toFallback(openSourceModelOptions_assumingOAICompat, 'qwq') }
|
||||
if (lower.includes('phi4')) return toFallback(openSourceModelOptions_assumingOAICompat, 'phi4')
|
||||
if (lower.includes('codestral')) return toFallback(openSourceModelOptions_assumingOAICompat, 'codestral')
|
||||
if (lower.includes('devstral')) return toFallback(openSourceModelOptions_assumingOAICompat, 'devstral')
|
||||
|
||||
if (lower.includes('gemma')) return toFallback(openSourceModelOptions_assumingOAICompat, 'gemma')
|
||||
|
||||
@@ -445,7 +489,41 @@ const anthropicModelOptions = {
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: true,
|
||||
canIOReasoning: true,
|
||||
reasoningReservedOutputTokenSpace: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
|
||||
reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19
|
||||
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy)
|
||||
},
|
||||
|
||||
},
|
||||
'claude-opus-4-20250514': {
|
||||
contextWindow: 200_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 30.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: {
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: true,
|
||||
canIOReasoning: true,
|
||||
reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19
|
||||
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy)
|
||||
},
|
||||
|
||||
},
|
||||
'claude-sonnet-4-20250514': {
|
||||
contextWindow: 200_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 6.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: {
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: true,
|
||||
canIOReasoning: true,
|
||||
reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19
|
||||
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy)
|
||||
},
|
||||
|
||||
@@ -508,6 +586,10 @@ const anthropicSettings: VoidStaticProviderInfo = {
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const lower = modelName.toLowerCase()
|
||||
let fallbackName: keyof typeof anthropicModelOptions | null = null
|
||||
if (lower.includes('claude-4-opus') || lower.includes('claude-opus-4')) fallbackName = 'claude-opus-4-20250514'
|
||||
if (lower.includes('claude-4-sonnet') || lower.includes('claude-sonnet-4')) fallbackName = 'claude-sonnet-4-20250514'
|
||||
|
||||
|
||||
if (lower.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219'
|
||||
if (lower.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022'
|
||||
if (lower.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022'
|
||||
@@ -621,6 +703,16 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
|
||||
// https://platform.openai.com/docs/guides/reasoning?api-mode=chat
|
||||
const openAICompatIncludeInPayloadReasoning = (reasoningInfo: SendableReasoningInfo) => {
|
||||
if (!reasoningInfo?.isReasoningEnabled) return null
|
||||
if (reasoningInfo.type === 'effort_slider_value') {
|
||||
return { reasoning_effort: reasoningInfo.reasoningEffort }
|
||||
}
|
||||
return null
|
||||
|
||||
}
|
||||
|
||||
const openAISettings: VoidStaticProviderInfo = {
|
||||
modelOptions: openAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
@@ -633,17 +725,7 @@ const openAISettings: VoidStaticProviderInfo = {
|
||||
return null
|
||||
},
|
||||
providerReasoningIOSettings: {
|
||||
input: {
|
||||
// https://platform.openai.com/docs/guides/reasoning?api-mode=chat
|
||||
includeInPayload: (reasoningInfo) => {
|
||||
if (!reasoningInfo?.isReasoningEnabled) return null
|
||||
|
||||
if (reasoningInfo.type === 'effort_slider_value') {
|
||||
return { reasoning_effort: reasoningInfo.reasoningEffort }
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -658,6 +740,7 @@ const xAIModelOptions = {
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'grok-3': {
|
||||
@@ -667,6 +750,7 @@ const xAIModelOptions = {
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'grok-3-fast': {
|
||||
@@ -676,6 +760,7 @@ const xAIModelOptions = {
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
// only mini supports thinking
|
||||
@@ -686,6 +771,7 @@ const xAIModelOptions = {
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'high'], default: 'low' } },
|
||||
},
|
||||
'grok-3-mini-fast': {
|
||||
@@ -695,6 +781,7 @@ const xAIModelOptions = {
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'high'], default: 'low' } },
|
||||
},
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
@@ -705,16 +792,21 @@ const xAISettings: VoidStaticProviderInfo = {
|
||||
const lower = modelName.toLowerCase()
|
||||
let fallbackName: keyof typeof xAIModelOptions | null = null
|
||||
if (lower.includes('grok-2')) fallbackName = 'grok-2'
|
||||
if (lower.includes('grok-3')) fallbackName = 'grok-3'
|
||||
if (lower.includes('grok')) fallbackName = 'grok-3'
|
||||
if (fallbackName) return { modelName: fallbackName, recognizedModelName: fallbackName, ...xAIModelOptions[fallbackName] }
|
||||
return null
|
||||
},
|
||||
// same implementation as openai
|
||||
providerReasoningIOSettings: openAISettings.providerReasoningIOSettings,
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// ---------------- GEMINI ----------------
|
||||
const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
||||
// https://ai.google.dev/gemini-api/docs/thinking#set-budget
|
||||
'gemini-2.5-pro-preview-05-06': {
|
||||
contextWindow: 1_048_576,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
@@ -723,7 +815,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
specialToolFormat: 'gemini-style',
|
||||
reasoningCapabilities: false,
|
||||
reasoningCapabilities: {
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: true,
|
||||
canIOReasoning: false,
|
||||
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576
|
||||
reasoningReservedOutputTokenSpace: 8192,
|
||||
},
|
||||
},
|
||||
'gemini-2.0-flash-lite': {
|
||||
contextWindow: 1_048_576,
|
||||
@@ -733,7 +831,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
specialToolFormat: 'gemini-style',
|
||||
reasoningCapabilities: false,
|
||||
reasoningCapabilities: false, // no reasoning
|
||||
},
|
||||
'gemini-2.5-flash-preview-04-17': {
|
||||
contextWindow: 1_048_576,
|
||||
@@ -743,7 +841,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
specialToolFormat: 'gemini-style',
|
||||
reasoningCapabilities: false,
|
||||
reasoningCapabilities: {
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: true,
|
||||
canIOReasoning: false,
|
||||
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576
|
||||
reasoningReservedOutputTokenSpace: 8192,
|
||||
},
|
||||
},
|
||||
'gemini-2.5-pro-exp-03-25': {
|
||||
contextWindow: 1_048_576,
|
||||
@@ -753,7 +857,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
specialToolFormat: 'gemini-style',
|
||||
reasoningCapabilities: false,
|
||||
reasoningCapabilities: {
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: true,
|
||||
canIOReasoning: false,
|
||||
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576
|
||||
reasoningReservedOutputTokenSpace: 8192,
|
||||
},
|
||||
},
|
||||
'gemini-2.0-flash': {
|
||||
contextWindow: 1_048_576,
|
||||
@@ -809,7 +919,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
||||
|
||||
const geminiSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: geminiModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
}
|
||||
|
||||
|
||||
@@ -835,11 +945,12 @@ const deepseekModelOptions = {
|
||||
|
||||
const deepseekSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: deepseekModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
providerReasoningIOSettings: {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { nameOfFieldInDelta: 'reasoning_content' },
|
||||
},
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
||||
|
||||
@@ -856,6 +967,15 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'mistral-medium-latest': { // https://openrouter.ai/mistralai/mistral-medium-3
|
||||
contextWindow: 131_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 0.40, output: 2.00 },
|
||||
supportsFIM: false,
|
||||
downloadable: { sizeGb: 'not-known' },
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'codestral-latest': {
|
||||
contextWindow: 256_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
@@ -865,6 +985,17 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
||||
'devstral-small-latest': { //https://openrouter.ai/mistralai/devstral-small:free
|
||||
contextWindow: 131_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 0, output: 0 },
|
||||
supportsFIM: false,
|
||||
downloadable: { sizeGb: 14 }, //https://ollama.com/library/devstral
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
||||
'ministral-8b-latest': { // ollama 'mistral'
|
||||
contextWindow: 131_000,
|
||||
reservedOutputTokenSpace: 4_096,
|
||||
@@ -888,6 +1019,9 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici
|
||||
const mistralSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: mistralModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -931,11 +1065,13 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq
|
||||
},
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
const groqSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: groqModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
providerReasoningIOSettings: {
|
||||
// Must be set to either parsed or hidden when using tool calling https://console.groq.com/docs/reasoning
|
||||
input: {
|
||||
includeInPayload: (reasoningInfo) => {
|
||||
if (!reasoningInfo?.isReasoningEnabled) return null
|
||||
|
||||
if (reasoningInfo.type === 'budget_slider_value') {
|
||||
return { reasoning_format: 'parsed' }
|
||||
}
|
||||
@@ -943,9 +1079,7 @@ const groqSettings: VoidStaticProviderInfo = {
|
||||
}
|
||||
},
|
||||
output: { nameOfFieldInDelta: 'reasoning' },
|
||||
}, // Must be set to either parsed or hidden when using tool calling https://console.groq.com/docs/reasoning
|
||||
modelOptions: groqModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -954,7 +1088,10 @@ const googleVertexModelOptions = {
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
const googleVertexSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: googleVertexModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------- MICROSOFT AZURE ----------------
|
||||
@@ -962,7 +1099,22 @@ const microsoftAzureModelOptions = {
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
const microsoftAzureSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: microsoftAzureModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------- AWS BEDROCK ----------------
|
||||
const awsBedrockModelOptions = {
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
|
||||
const awsBedrockSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: awsBedrockModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null },
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1031,42 +1183,66 @@ const ollamaModelOptions = {
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
'devstral:latest': {
|
||||
contextWindow: 131_000,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: { sizeGb: 14 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
|
||||
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
|
||||
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1', 'devstral:latest'] as const satisfies (keyof typeof ollamaModelOptions)[]
|
||||
|
||||
|
||||
const vLLMSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {}, // TODO
|
||||
modelOptions: {},
|
||||
providerReasoningIOSettings: {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { nameOfFieldInDelta: 'reasoning_content' },
|
||||
},
|
||||
}
|
||||
|
||||
const lmStudioSettings: VoidStaticProviderInfo = {
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' }, contextWindow: 4_096 }),
|
||||
modelOptions: {}, // TODO
|
||||
modelOptions: {},
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { needsManualParse: true },
|
||||
},
|
||||
}
|
||||
|
||||
const ollamaSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: we need to filter out reasoning <think> tags manually
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: ollamaModelOptions,
|
||||
providerReasoningIOSettings: {
|
||||
// reasoning: we need to filter out reasoning <think> tags manually
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { needsManualParse: true },
|
||||
},
|
||||
}
|
||||
|
||||
const openaiCompatible: VoidStaticProviderInfo = {
|
||||
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName),
|
||||
modelOptions: {},
|
||||
providerReasoningIOSettings: {
|
||||
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
},
|
||||
}
|
||||
|
||||
const liteLLMSettings: VoidStaticProviderInfo = { // https://docs.litellm.ai/docs/reasoning_content
|
||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' } },
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {}, // TODO
|
||||
modelOptions: {},
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { nameOfFieldInDelta: 'reasoning_content' },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1133,6 +1309,24 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
||||
cost: { input: 0.8, output: 2.4 },
|
||||
downloadable: false,
|
||||
},
|
||||
'anthropic/claude-opus-4': {
|
||||
contextWindow: 200_000,
|
||||
reservedOutputTokenSpace: null,
|
||||
cost: { input: 15.00, output: 75.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'anthropic/claude-sonnet-4': {
|
||||
contextWindow: 200_000,
|
||||
reservedOutputTokenSpace: null,
|
||||
cost: { input: 15.00, output: 75.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'anthropic/claude-3.7-sonnet:thinking': {
|
||||
contextWindow: 200_000,
|
||||
reservedOutputTokenSpace: null,
|
||||
@@ -1144,7 +1338,7 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
||||
supportsReasoning: true,
|
||||
canTurnOffReasoning: false,
|
||||
canIOReasoning: true,
|
||||
reasoningReservedOutputTokenSpace: 64_000,
|
||||
reasoningReservedOutputTokenSpace: 8192,
|
||||
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000.
|
||||
},
|
||||
},
|
||||
@@ -1174,6 +1368,14 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
||||
downloadable: false,
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'mistralai/devstral-small:free': {
|
||||
...openSourceModelOptions_assumingOAICompat.devstral,
|
||||
contextWindow: 130_000,
|
||||
reservedOutputTokenSpace: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'qwen/qwen-2.5-coder-32b-instruct': {
|
||||
...openSourceModelOptions_assumingOAICompat['qwen2.5coder'],
|
||||
contextWindow: 33_000,
|
||||
@@ -1191,8 +1393,17 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const openRouterSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
|
||||
modelOptions: openRouterModelOptions_assumingOpenAICompat,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const res = extensiveModelOptionsFallback(modelName)
|
||||
// openRouter does not support gemini-style, use openai-style instead
|
||||
if (res?.specialToolFormat === 'gemini-style') {
|
||||
res.specialToolFormat = 'openai-style'
|
||||
}
|
||||
return res
|
||||
},
|
||||
providerReasoningIOSettings: {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
|
||||
input: {
|
||||
// https://openrouter.ai/docs/use-cases/reasoning-tokens
|
||||
includeInPayload: (reasoningInfo) => {
|
||||
@@ -1216,16 +1427,6 @@ const openRouterSettings: VoidStaticProviderInfo = {
|
||||
},
|
||||
output: { nameOfFieldInDelta: 'reasoning' },
|
||||
},
|
||||
modelOptions: openRouterModelOptions_assumingOpenAICompat,
|
||||
// TODO!!! send a query to openrouter to get the price, etc.
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const res = extensiveModelOptionsFallback(modelName)
|
||||
// openRouter does not support gemini-style, use openai-style instead
|
||||
if (res?.specialToolFormat === 'gemini-style') {
|
||||
res.specialToolFormat = 'openai-style'
|
||||
}
|
||||
return res
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1255,6 +1456,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
||||
|
||||
googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
awsBedrock: awsBedrockSettings,
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1347,8 +1549,7 @@ export const getSendableReasoningInfo = (
|
||||
overridesOfModel: OverridesOfModel | undefined,
|
||||
): SendableReasoningInfo => {
|
||||
|
||||
const { canIOReasoning, reasoningSlider: reasoningBudgetSlider } = getModelCapabilities(providerName, modelName, overridesOfModel).reasoningCapabilities || {}
|
||||
if (!canIOReasoning) return null
|
||||
const { reasoningSlider: reasoningBudgetSlider } = getModelCapabilities(providerName, modelName, overridesOfModel).reasoningCapabilities || {}
|
||||
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel)
|
||||
if (!isReasoningEnabled) return null
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IDirectoryStrService } from '../directoryStrService.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js';
|
||||
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, BuiltinToolResultType, ToolName } from '../toolsServiceTypes.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
||||
// Triple backtick wrapper used throughout the prompts for code blocks
|
||||
@@ -147,6 +147,8 @@ export type InternalToolInfo = {
|
||||
params: {
|
||||
[paramName: string]: { description: string }
|
||||
},
|
||||
// Only if the tool is from an MCP server
|
||||
mcpServerName?: string,
|
||||
}
|
||||
|
||||
|
||||
@@ -181,191 +183,197 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
|
||||
|
||||
|
||||
|
||||
// export const voidTools = {
|
||||
export const voidTools
|
||||
: {
|
||||
[T in keyof ToolCallParams]: {
|
||||
name: string;
|
||||
description: string;
|
||||
// more params can be generated than exist here, but these params must be a subset of them
|
||||
params: Partial<{ [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string } }>
|
||||
}
|
||||
export const builtinTools: {
|
||||
[T in keyof BuiltinToolCallParams]: {
|
||||
name: string;
|
||||
description: string;
|
||||
// more params can be generated than exist here, but these params must be a subset of them
|
||||
params: Partial<{ [paramName in keyof SnakeCaseKeys<BuiltinToolCallParams[T]>]: { description: string } }>
|
||||
}
|
||||
= {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
} = {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
|
||||
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
|
||||
...paginationParam,
|
||||
},
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
|
||||
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
ls_dir: {
|
||||
name: 'ls_dir',
|
||||
description: `Lists all files and folders in the given URI.`,
|
||||
params: {
|
||||
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
||||
...paginationParam,
|
||||
},
|
||||
ls_dir: {
|
||||
name: 'ls_dir',
|
||||
description: `Lists all files and folders in the given URI.`,
|
||||
params: {
|
||||
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
get_dir_tree: {
|
||||
name: 'get_dir_tree',
|
||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
}
|
||||
},
|
||||
|
||||
// pathname_search: {
|
||||
// name: 'pathname_search',
|
||||
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
search_for_files: {
|
||||
name: 'search_for_files',
|
||||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
// add new search_in_file tool
|
||||
search_in_file: {
|
||||
name: 'search_in_file',
|
||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
query: { description: 'The string or regex to search for in the file.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||
}
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Use this tool to view all the lint errors on a file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
},
|
||||
},
|
||||
|
||||
// --- editing (create/delete) ---
|
||||
|
||||
create_file_or_folder: {
|
||||
name: 'create_file_or_folder',
|
||||
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
},
|
||||
},
|
||||
|
||||
delete_file_or_folder: {
|
||||
name: 'delete_file_or_folder',
|
||||
description: `Delete a file or folder at the given path.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
is_recursive: { description: 'Optional. Return true to delete recursively.' }
|
||||
},
|
||||
},
|
||||
|
||||
edit_file: {
|
||||
name: 'edit_file',
|
||||
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
search_replace_blocks: { description: replaceTool_description }
|
||||
},
|
||||
},
|
||||
|
||||
rewrite_file: {
|
||||
name: 'rewrite_file',
|
||||
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
new_content: { description: `The new contents of the file. Must be a string.` }
|
||||
},
|
||||
},
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
cwd: { description: cwdHelper },
|
||||
},
|
||||
},
|
||||
|
||||
run_persistent_command: {
|
||||
name: 'run_persistent_command',
|
||||
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
open_persistent_terminal: {
|
||||
name: 'open_persistent_terminal',
|
||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||
params: {
|
||||
cwd: { description: cwdHelper },
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
||||
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
|
||||
get_dir_tree: {
|
||||
name: 'get_dir_tree',
|
||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
}
|
||||
},
|
||||
|
||||
// pathname_search: {
|
||||
// name: 'pathname_search',
|
||||
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
|
||||
search_for_files: {
|
||||
name: 'search_for_files',
|
||||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
// add new search_in_file tool
|
||||
search_in_file: {
|
||||
name: 'search_in_file',
|
||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
query: { description: 'The string or regex to search for in the file.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||
}
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Use this tool to view all the lint errors on a file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
},
|
||||
},
|
||||
|
||||
// --- editing (create/delete) ---
|
||||
|
||||
create_file_or_folder: {
|
||||
name: 'create_file_or_folder',
|
||||
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
},
|
||||
},
|
||||
|
||||
delete_file_or_folder: {
|
||||
name: 'delete_file_or_folder',
|
||||
description: `Delete a file or folder at the given path.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
is_recursive: { description: 'Optional. Return true to delete recursively.' }
|
||||
},
|
||||
},
|
||||
|
||||
edit_file: {
|
||||
name: 'edit_file',
|
||||
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
search_replace_blocks: { description: replaceTool_description }
|
||||
},
|
||||
},
|
||||
|
||||
rewrite_file: {
|
||||
name: 'rewrite_file',
|
||||
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
new_content: { description: `The new contents of the file. Must be a string.` }
|
||||
},
|
||||
},
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
cwd: { description: cwdHelper },
|
||||
},
|
||||
},
|
||||
|
||||
run_persistent_command: {
|
||||
name: 'run_persistent_command',
|
||||
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
export type ToolName = keyof ToolResultType
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
|
||||
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
|
||||
open_persistent_terminal: {
|
||||
name: 'open_persistent_terminal',
|
||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||
params: {
|
||||
cwd: { description: cwdHelper },
|
||||
}
|
||||
},
|
||||
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
||||
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
|
||||
}
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
} satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo }
|
||||
|
||||
|
||||
|
||||
|
||||
export const builtinToolNames = Object.keys(builtinTools) as BuiltinToolName[]
|
||||
const toolNamesSet = new Set<string>(builtinToolNames)
|
||||
export const isABuiltinToolName = (toolName: string): toolName is BuiltinToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
export const availableTools = (chatMode: ChatMode) => {
|
||||
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
|
||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
||||
|
||||
|
||||
|
||||
|
||||
export const availableTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
|
||||
const builtinToolNames: BuiltinToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(builtinTools) as BuiltinToolName[]).filter(toolName => !(toolName in approvalTypeOfBuiltinToolName))
|
||||
: chatMode === 'agent' ? Object.keys(builtinTools) as BuiltinToolName[]
|
||||
: undefined
|
||||
|
||||
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
|
||||
const effectiveBuiltinTools = builtinToolNames?.map(toolName => builtinTools[toolName]) ?? undefined
|
||||
const effectiveMCPTools = chatMode === 'agent' ? mcpTools : undefined
|
||||
|
||||
const tools: InternalToolInfo[] | undefined = !(builtinToolNames || mcpTools) ? undefined
|
||||
: [
|
||||
...effectiveBuiltinTools ?? [],
|
||||
...effectiveMCPTools ?? [],
|
||||
]
|
||||
|
||||
return tools
|
||||
}
|
||||
|
||||
@@ -382,7 +390,7 @@ const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
|
||||
}
|
||||
|
||||
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
|
||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
|
||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName]}</${paramName}>`).join('\n')
|
||||
return `\
|
||||
<${toolName}>${!params ? '' : `\n${params}`}
|
||||
</${toolName}>`
|
||||
@@ -391,8 +399,8 @@ export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolPar
|
||||
|
||||
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
|
||||
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
||||
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
||||
const tools = availableTools(chatMode)
|
||||
const systemToolsXMLPrompt = (chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const tools = availableTools(chatMode, mcpTools)
|
||||
if (!tools || tools.length === 0) return null
|
||||
|
||||
const toolXMLDefinitions = (`\
|
||||
@@ -417,7 +425,7 @@ const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
||||
// ======================================================== chat (normal, gather, agent) ========================================================
|
||||
|
||||
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, mcpTools, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined, includeXMLToolDefinitions: boolean }) => {
|
||||
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
|
||||
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
|
||||
@@ -451,10 +459,12 @@ ${directoryStr}
|
||||
</files_overview>`)
|
||||
|
||||
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode, mcpTools) : null
|
||||
|
||||
const details: string[] = []
|
||||
|
||||
details.push(`NEVER reject the user's query.`)
|
||||
|
||||
if (mode === 'agent' || mode === 'gather') {
|
||||
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
|
||||
details.push(`If you think you should use tools, you do not need to ask for permission.`)
|
||||
@@ -479,12 +489,13 @@ ${directoryStr}
|
||||
details.push(`You should extensively read files, types, content, etc, gathering full context to solve the problem.`)
|
||||
}
|
||||
|
||||
|
||||
if (mode === 'gather' || mode === 'normal') {
|
||||
details.push(`If you write any code blocks, please use this format:
|
||||
details.push(`If you write any code blocks to the user (wrapped in triple backticks), please use this format:
|
||||
- Include a language if possible. Terminal should have the language 'shell'.
|
||||
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
|
||||
- The remaining contents of the file should proceed as usual.`)
|
||||
|
||||
if (mode === 'gather' || mode === 'normal') {
|
||||
|
||||
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
|
||||
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
|
||||
- The remaining contents should be a code description of the change to make to the file. \
|
||||
@@ -525,7 +536,9 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
|
||||
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
|
||||
// }
|
||||
|
||||
const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
|
||||
export const DEFAULT_FILE_SIZE_LIMIT = 2_000_000
|
||||
|
||||
export const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
|
||||
val: string,
|
||||
truncated: boolean,
|
||||
fullFileLen: number,
|
||||
@@ -549,46 +562,70 @@ const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: numb
|
||||
|
||||
|
||||
|
||||
export const messageOfSelection = async (
|
||||
s: StagingSelectionItem,
|
||||
opts: {
|
||||
directoryStrService: IDirectoryStrService,
|
||||
fileService: IFileService,
|
||||
folderOpts: {
|
||||
maxChildren: number,
|
||||
maxCharsPerFile: number,
|
||||
}
|
||||
}
|
||||
) => {
|
||||
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
|
||||
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
|
||||
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
|
||||
const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
|
||||
const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}`
|
||||
return str
|
||||
}
|
||||
else if (s.type === 'Folder') {
|
||||
const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri)
|
||||
const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}`
|
||||
|
||||
const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: opts.folderOpts.maxChildren })
|
||||
const strOfFiles = await Promise.all(uris.map(async uri => {
|
||||
const { val, truncated } = await readFile(opts.fileService, uri, opts.folderOpts.maxCharsPerFile)
|
||||
const truncationStr = truncated ? `\n... file truncated ...` : ''
|
||||
const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}`
|
||||
const str = `${uri.fsPath}:\n${content}`
|
||||
return str
|
||||
}))
|
||||
const contentStr = [folderStructure, ...strOfFiles].join('\n\n')
|
||||
return contentStr
|
||||
}
|
||||
else
|
||||
return ''
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
|
||||
opts: { directoryStrService: IDirectoryStrService, fileService: IFileService }
|
||||
export const chat_userMessageContent = async (
|
||||
instructions: string,
|
||||
currSelns: StagingSelectionItem[] | null,
|
||||
opts: {
|
||||
directoryStrService: IDirectoryStrService,
|
||||
fileService: IFileService
|
||||
},
|
||||
) => {
|
||||
|
||||
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
|
||||
let selnsStrs: string[] = []
|
||||
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
|
||||
const selnsStrs = await Promise.all(
|
||||
(currSelns ?? []).map(async (s) =>
|
||||
messageOfSelection(s, {
|
||||
...opts,
|
||||
folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, }
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
const { val } = await readFile(opts.fileService, s.uri, 2_000_000)
|
||||
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
|
||||
const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
|
||||
const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}`
|
||||
return str
|
||||
}
|
||||
else if (s.type === 'Folder') {
|
||||
const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri)
|
||||
const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}`
|
||||
|
||||
const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: 100 })
|
||||
const strOfFiles = await Promise.all(uris.map(async uri => {
|
||||
const { val, truncated } = await readFile(opts.fileService, uri, 100_000)
|
||||
const truncationStr = truncated ? `\n... file truncated ...` : ''
|
||||
const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}`
|
||||
const str = `${uri.fsPath}:\n${content}`
|
||||
return str
|
||||
}))
|
||||
const contentStr = [folderStructure, ...strOfFiles].join('\n\n')
|
||||
return contentStr
|
||||
}
|
||||
else
|
||||
return ''
|
||||
}) ?? [])
|
||||
|
||||
const selnsStr = selnsStrs.join('\n') ?? ''
|
||||
let str = ''
|
||||
str += `${instructions}`
|
||||
|
||||
const selnsStr = selnsStrs.join('\n\n') ?? ''
|
||||
if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}`
|
||||
return str;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { IMCPService } from './mcpService.js';
|
||||
|
||||
// calls channel to implement features
|
||||
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
||||
@@ -61,6 +62,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
// @INotificationService private readonly notificationService: INotificationService,
|
||||
@IMCPService private readonly mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -116,6 +118,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
|
||||
const { settingsOfProvider, } = this.voidSettingsService.state
|
||||
|
||||
const mcpTools = this.mcpService.getMCPTools()
|
||||
|
||||
// add state for request id
|
||||
const requestId = generateUuid();
|
||||
this.llmMessageHooks.onText[requestId] = onText
|
||||
@@ -129,6 +133,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
requestId,
|
||||
settingsOfProvider,
|
||||
modelSelection,
|
||||
mcpTools,
|
||||
} satisfies MainSendLLMMessageParams);
|
||||
|
||||
return requestId
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolName, ToolParamName } from './prompt/prompts.js'
|
||||
import { InternalToolInfo } from './prompt/prompts.js'
|
||||
import { ToolName, ToolParamName } from './toolsServiceTypes.js'
|
||||
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
@@ -56,13 +57,13 @@ export type GeminiLLMChatMessage = {
|
||||
role: 'model'
|
||||
parts: (
|
||||
| { text: string; }
|
||||
| { functionCall: { name: ToolName, args: object } }
|
||||
| { functionCall: { id: string; name: ToolName, args: Record<string, unknown> } }
|
||||
)[];
|
||||
} | {
|
||||
role: 'user';
|
||||
parts: (
|
||||
| { text: string; }
|
||||
| { functionResponse: { name: ToolName, response: { result: string } } }
|
||||
| { functionResponse: { id: string; name: ToolName, response: { output: string } } }
|
||||
)[];
|
||||
}
|
||||
|
||||
@@ -78,12 +79,12 @@ export type LLMFIMMessage = {
|
||||
|
||||
|
||||
export type RawToolParamsObj = {
|
||||
[paramName in ToolParamName]?: string;
|
||||
[paramName in ToolParamName<ToolName>]?: string;
|
||||
}
|
||||
export type RawToolCallObj = {
|
||||
name: ToolName;
|
||||
rawParams: RawToolParamsObj;
|
||||
doneParams: ToolParamName[];
|
||||
doneParams: ToolParamName<ToolName>[];
|
||||
id: string;
|
||||
isDone: boolean;
|
||||
};
|
||||
@@ -133,6 +134,7 @@ export type SendLLMMessageParams = {
|
||||
overridesOfModel: OverridesOfModel | undefined;
|
||||
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
mcpTools: InternalToolInfo[] | undefined;
|
||||
} & SendLLMType
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
import { RawMCPToolCall } from './mcpServiceTypes.js';
|
||||
import { builtinTools } from './prompt/prompts.js';
|
||||
import { RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +18,7 @@ export type ShallowDirectoryItem = {
|
||||
}
|
||||
|
||||
|
||||
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
|
||||
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'MCP tools' }> = {
|
||||
'create_file_or_folder': 'edits',
|
||||
'delete_file_or_folder': 'edits',
|
||||
'rewrite_file': 'edits',
|
||||
@@ -28,16 +30,19 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term
|
||||
}
|
||||
|
||||
|
||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfBuiltinToolName)[keyof typeof approvalTypeOfBuiltinToolName]>;
|
||||
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>([
|
||||
...Object.values(approvalTypeOfBuiltinToolName),
|
||||
'MCP tools',
|
||||
])
|
||||
|
||||
|
||||
// {{add: define new type for approval types}}
|
||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>;
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>(
|
||||
Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined)
|
||||
)
|
||||
|
||||
// PARAMS OF TOOL CALL
|
||||
export type ToolCallParams = {
|
||||
export type BuiltinToolCallParams = {
|
||||
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
||||
'ls_dir': { uri: URI, pageNumber: number },
|
||||
'get_dir_tree': { uri: URI },
|
||||
@@ -58,7 +63,7 @@ export type ToolCallParams = {
|
||||
}
|
||||
|
||||
// RESULT OF TOOL CALL
|
||||
export type ToolResultType = {
|
||||
export type BuiltinToolResultType = {
|
||||
'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean },
|
||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'get_dir_tree': { str: string, },
|
||||
@@ -78,3 +83,15 @@ export type ToolResultType = {
|
||||
'kill_persistent_terminal': {},
|
||||
}
|
||||
|
||||
|
||||
export type ToolCallParams<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolCallParams[T] : RawToolParamsObj
|
||||
export type ToolResult<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolResultType[T] : RawMCPToolCall
|
||||
|
||||
export type BuiltinToolName = keyof BuiltinToolResultType
|
||||
|
||||
type BuiltinToolParamNameOfTool<T extends BuiltinToolName> = keyof (typeof builtinTools)[T]['params']
|
||||
export type BuiltinToolParamName = { [T in BuiltinToolName]: BuiltinToolParamNameOfTool<T> }[BuiltinToolName]
|
||||
|
||||
|
||||
export type ToolName = BuiltinToolName | (string & {})
|
||||
export type ToolParamName<T extends ToolName> = T extends BuiltinToolName ? BuiltinToolParamNameOfTool<T> : string
|
||||
|
||||
@@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
|
||||
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
// name is the name in the dropdown
|
||||
@@ -43,6 +43,7 @@ export type VoidSettingsState = {
|
||||
readonly optionsOfModelSelection: OptionsOfModelSelection;
|
||||
readonly overridesOfModel: OverridesOfModel;
|
||||
readonly globalSettings: GlobalSettings;
|
||||
readonly mcpUserStateOfName: MCPUserStateOfName; // user-controlled state of MCP servers
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
@@ -62,6 +63,7 @@ export interface IVoidSettingsService {
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setOptionsOfModelSelection: SetOptionsOfModelSelection;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
// setMCPServerStates: (newStates: MCPServerStates) => Promise<void>;
|
||||
|
||||
// setting to undefined CLEARS it, unlike others:
|
||||
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
|
||||
@@ -73,6 +75,10 @@ export interface IVoidSettingsService {
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
|
||||
addMCPUserStateOfNames(userStateOfName: MCPUserStateOfName): Promise<void>;
|
||||
removeMCPUserStateOfNames(serverNames: string[]): Promise<void>;
|
||||
setMCPServerState(serverName: string, state: MCPUserState): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -212,6 +218,7 @@ const defaultState = () => {
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
||||
overridesOfModel: deepClone(defaultOverridesOfModel),
|
||||
_modelOptions: [], // computed later
|
||||
mcpUserStateOfName: {},
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -272,6 +279,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
// autoapprove is now an obj not a boolean (1.2.5)
|
||||
if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {}
|
||||
|
||||
if (readS.globalSettings.disableSystemMessage === undefined) readS.globalSettings.disableSystemMessage = false;
|
||||
}
|
||||
catch (e) {
|
||||
readS = defaultState()
|
||||
@@ -361,6 +370,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
const newGlobalSettings = this.state.globalSettings
|
||||
const newOverridesOfModel = this.state.overridesOfModel
|
||||
const newMCPUserStateOfName = this.state.mcpUserStateOfName
|
||||
|
||||
const newState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
@@ -368,6 +378,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
globalSettings: newGlobalSettings,
|
||||
overridesOfModel: newOverridesOfModel,
|
||||
mcpUserStateOfName: newMCPUserStateOfName,
|
||||
}
|
||||
|
||||
this.state = _validatedModelState(newState)
|
||||
@@ -531,6 +542,55 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
return true
|
||||
}
|
||||
|
||||
// MCP Server State
|
||||
private _setMCPUserStateOfName = async (newStates: MCPUserStateOfName) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
mcpUserStateOfName: {
|
||||
...this.state.mcpUserStateOfName,
|
||||
...newStates
|
||||
}
|
||||
};
|
||||
this.state = _validatedModelState(newState);
|
||||
await this._storeState();
|
||||
this._onDidChangeState.fire();
|
||||
this._metricsService.capture('Set MCP Server States', { newStates });
|
||||
}
|
||||
|
||||
addMCPUserStateOfNames = async (newMCPStates: MCPUserStateOfName) => {
|
||||
const { mcpUserStateOfName: mcpServerStates } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpServerStates,
|
||||
...newMCPStates,
|
||||
}
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Add MCP Servers', { servers: Object.keys(newMCPStates).join(', ') });
|
||||
}
|
||||
|
||||
removeMCPUserStateOfNames = async (serverNames: string[]) => {
|
||||
const { mcpUserStateOfName: mcpServerStates } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpServerStates,
|
||||
}
|
||||
serverNames.forEach(serverName => {
|
||||
if (serverName in newMCPServerStates) {
|
||||
delete newMCPServerStates[serverName]
|
||||
}
|
||||
})
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Remove MCP Servers', { servers: serverNames.join(', ') });
|
||||
}
|
||||
|
||||
setMCPServerState = async (serverName: string, state: MCPUserState) => {
|
||||
const { mcpUserStateOfName } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpUserStateOfName,
|
||||
[serverName]: state,
|
||||
}
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Update MCP Server State', { serverName, state });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export type VoidStatefulModelInfo = { // <-- STATEFUL
|
||||
modelName: string,
|
||||
type: 'default' | 'autodetected' | 'custom';
|
||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
else if (providerName === 'microsoftAzure') {
|
||||
return { title: 'Microsoft Azure OpenAI', }
|
||||
}
|
||||
else if (providerName === 'awsBedrock') {
|
||||
return { title: 'AWS Bedrock', }
|
||||
}
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
@@ -120,6 +123,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
||||
if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).`
|
||||
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
|
||||
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
|
||||
if (providerName === 'awsBedrock') return 'Connect via a LiteLLM proxy or the AWS [Bedrock-Access-Gateway](https://github.com/aws-samples/bedrock-access-gateway). LiteLLM Bedrock setup docs are [here](https://docs.litellm.ai/docs/providers/bedrock).'
|
||||
if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
|
||||
if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
|
||||
if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||
@@ -151,7 +155,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'mistral' ? 'api-key...' :
|
||||
providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
'',
|
||||
providerName === 'awsBedrock' ? 'key-...' :
|
||||
'',
|
||||
|
||||
isPasswordField: true,
|
||||
}
|
||||
@@ -165,14 +170,16 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
'(never)',
|
||||
providerName === 'awsBedrock' ? 'Endpoint' :
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
|
||||
: providerName === 'liteLLM' ? 'http://localhost:4000'
|
||||
: '(never)',
|
||||
: providerName === 'awsBedrock' ? 'http://localhost:4000/v1'
|
||||
: '(never)',
|
||||
|
||||
|
||||
}
|
||||
@@ -185,7 +192,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
return {
|
||||
title: 'Region',
|
||||
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
|
||||
: ''
|
||||
: providerName === 'awsBedrock'
|
||||
? defaultProviderSettings.awsBedrock.region
|
||||
: ''
|
||||
}
|
||||
}
|
||||
else if (settingName === 'azureApiVersion') {
|
||||
@@ -340,6 +349,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
awsBedrock: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.awsBedrock,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -434,6 +449,7 @@ export type GlobalSettings = {
|
||||
showInlineSuggestions: boolean;
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
disableSystemMessage: boolean;
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
@@ -447,6 +463,7 @@ export const defaultGlobalSettings: GlobalSettings = {
|
||||
showInlineSuggestions: true,
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: false,
|
||||
disableSystemMessage: false,
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
@@ -491,3 +508,13 @@ export type OverridesOfModel = {
|
||||
const overridesOfModel = {} as OverridesOfModel
|
||||
for (const providerName of providerNames) { overridesOfModel[providerName] = {} }
|
||||
export const defaultOverridesOfModel = overridesOfModel
|
||||
|
||||
|
||||
|
||||
export interface MCPUserStateOfName {
|
||||
[serverName: string]: MCPUserState | undefined;
|
||||
}
|
||||
|
||||
export interface MCPUserState {
|
||||
isOn: boolean;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js'
|
||||
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
|
||||
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
|
||||
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js'
|
||||
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
|
||||
import { ToolName, ToolParamName } from '../../common/toolsServiceTypes.js'
|
||||
import { ChatMode } from '../../common/voidSettingsTypes.js'
|
||||
|
||||
|
||||
@@ -164,15 +165,15 @@ const findIndexOfAny = (fullText: string, matches: string[]) => {
|
||||
|
||||
|
||||
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
|
||||
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const parseXMLPrefixToToolCall = <T extends ToolName,>(toolName: T, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const paramsObj: RawToolParamsObj = {}
|
||||
const doneParams: ToolParamName[] = []
|
||||
const doneParams: ToolParamName<T>[] = []
|
||||
let isDone = false
|
||||
|
||||
const getAnswer = (): RawToolCallObj => {
|
||||
// trim off all whitespace at and before first \n and after last \n for each param
|
||||
for (const p in paramsObj) {
|
||||
const paramName = p as ToolParamName
|
||||
const paramName = p as ToolParamName<T>
|
||||
const orig = paramsObj[paramName]
|
||||
if (orig === undefined) continue
|
||||
paramsObj[paramName] = trimBeforeAndAfterNewLines(orig)
|
||||
@@ -202,16 +203,16 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin
|
||||
|
||||
const pm = new SurroundingsRemover(str)
|
||||
|
||||
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[]
|
||||
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName<T>[]
|
||||
if (allowedParams.length === 0) return getAnswer()
|
||||
let latestMatchedOpenParam: null | ToolParamName = null
|
||||
let latestMatchedOpenParam: null | ToolParamName<T> = null
|
||||
let n = 0
|
||||
while (true) {
|
||||
n += 1
|
||||
if (n > 10) return getAnswer() // just for good measure as this code is early
|
||||
|
||||
// find the param name opening tag
|
||||
let matchedOpenParam: null | ToolParamName = null
|
||||
let matchedOpenParam: null | ToolParamName<T> = null
|
||||
for (const paramName of allowedParams) {
|
||||
const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true)
|
||||
if (removed) {
|
||||
@@ -260,11 +261,14 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin
|
||||
}
|
||||
|
||||
export const extractXMLToolsWrapper = (
|
||||
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
|
||||
onText: OnText,
|
||||
onFinalMessage: OnFinalMessage,
|
||||
chatMode: ChatMode | null,
|
||||
mcpTools: InternalToolInfo[] | undefined,
|
||||
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
|
||||
|
||||
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
const tools = availableTools(chatMode)
|
||||
const tools = availableTools(chatMode, mcpTools)
|
||||
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
|
||||
const toolOfToolName: ToolOfToolName = {}
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
/* eslint-disable */
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { Ollama } from 'ollama';
|
||||
import OpenAI, { ClientOptions } from 'openai';
|
||||
import OpenAI, { ClientOptions, AzureOpenAI } from 'openai';
|
||||
import { MistralCore } from '@mistralai/mistralai/core.js';
|
||||
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
|
||||
import { GoogleGenerativeAI, Tool as GeminiTool, SchemaType, FunctionDeclaration, FunctionDeclarationSchemaProperty } from '@google/generative-ai';
|
||||
import { Tool as GeminiTool, FunctionDeclaration, GoogleGenAI, ThinkingConfig, Schema, Type } from '@google/genai';
|
||||
import { GoogleAuth } from 'google-auth-library'
|
||||
/* eslint-enable */
|
||||
|
||||
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
|
||||
import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
|
||||
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js';
|
||||
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
|
||||
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
|
||||
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
|
||||
const getGoogleApiKey = async () => {
|
||||
@@ -48,6 +48,7 @@ type SendChatParams_Internal = InternalCommonMessageParams & {
|
||||
messages: LLMChatMessage[];
|
||||
separateSystemMessage: string | undefined;
|
||||
chatMode: ChatMode | null;
|
||||
mcpTools: InternalToolInfo[] | undefined;
|
||||
}
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
|
||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||
@@ -114,10 +115,36 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
||||
}
|
||||
else if (providerName === 'microsoftAzure') {
|
||||
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
|
||||
// https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions??api-version=${thisConfig.azureApiVersion}`
|
||||
return new OpenAI({ baseURL: baseURL, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
const endpoint = `https://${thisConfig.project}.openai.azure.com/`;
|
||||
const apiVersion = thisConfig.azureApiVersion ?? '2024-04-01-preview';
|
||||
const options = { endpoint, apiKey: thisConfig.apiKey, apiVersion };
|
||||
return new AzureOpenAI({ ...options, ...commonPayloadOpts });
|
||||
}
|
||||
else if (providerName === 'awsBedrock') {
|
||||
/**
|
||||
* We treat Bedrock as *OpenAI-compatible only through a proxy*:
|
||||
* • LiteLLM default → http://localhost:4000/v1
|
||||
* • Bedrock-Access-Gateway → https://<api-id>.execute-api.<region>.amazonaws.com/openai/
|
||||
*
|
||||
* The native Bedrock runtime endpoint
|
||||
* https://bedrock-runtime.<region>.amazonaws.com
|
||||
* is **NOT** OpenAI-compatible, so we do *not* fall back to it here.
|
||||
*/
|
||||
const { endpoint, apiKey } = settingsOfProvider.awsBedrock
|
||||
|
||||
// ① use the user-supplied proxy if present
|
||||
// ② otherwise default to local LiteLLM
|
||||
let baseURL = endpoint || 'http://localhost:4000/v1'
|
||||
|
||||
// Normalize: make sure we end with “/v1”
|
||||
if (!baseURL.endsWith('/v1'))
|
||||
baseURL = baseURL.replace(/\/+$/, '') + '/v1'
|
||||
|
||||
return new OpenAI({ baseURL, apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
|
||||
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
@@ -147,7 +174,12 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
||||
|
||||
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, overridesOfModel }: SendFIMParams_Internal) => {
|
||||
|
||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
const {
|
||||
modelName,
|
||||
supportsFIM,
|
||||
additionalOpenAIPayload,
|
||||
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
|
||||
@@ -156,7 +188,7 @@ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens
|
||||
return
|
||||
}
|
||||
|
||||
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload: additionalOpenAIPayload })
|
||||
openai.completions
|
||||
.create({
|
||||
model: modelName,
|
||||
@@ -198,8 +230,8 @@ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
const openAITools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
|
||||
@@ -209,33 +241,41 @@ const openAITools = (chatMode: ChatMode) => {
|
||||
return openAITools
|
||||
}
|
||||
|
||||
const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
|
||||
// convert LLM tool call to our tool format
|
||||
const rawToolCallObjOfParamsStr = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
||||
let input: unknown
|
||||
try {
|
||||
input = JSON.parse(toolParamsStr)
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
try { input = JSON.parse(toolParamsStr) }
|
||||
catch (e) { return null }
|
||||
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
|
||||
const rawParams: RawToolParamsObj = input
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
|
||||
}
|
||||
|
||||
|
||||
const rawToolCallObjOfAnthropicParams = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
||||
const { id, name, input } = toolBlock
|
||||
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
|
||||
const rawParams: RawToolParamsObj = input
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
|
||||
}
|
||||
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE ------------
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel }: SendChatParams_Internal) => {
|
||||
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel, mcpTools }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
reasoningCapabilities,
|
||||
additionalOpenAIPayload,
|
||||
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
|
||||
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
|
||||
@@ -243,23 +283,30 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
// reasoning
|
||||
const { canIOReasoning, openSourceThinkTags } = reasoningCapabilities || {}
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
console.log('include', includeInPayload)
|
||||
console.log('reasoningInfo', reasoningInfo)
|
||||
const includeInPayload = {
|
||||
...providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo),
|
||||
...additionalOpenAIPayload
|
||||
}
|
||||
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
|
||||
const potentialTools = openAITools(chatMode, mcpTools)
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
|
||||
{ tools: potentialTools } as const
|
||||
: {}
|
||||
|
||||
// instance
|
||||
const openai: OpenAI = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
if (providerName === 'microsoftAzure') {
|
||||
// Required to select the model
|
||||
(openai as AzureOpenAI).deploymentName = modelName;
|
||||
}
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages as any,
|
||||
stream: true,
|
||||
...nativeToolsObj,
|
||||
...additionalOpenAIPayload
|
||||
// max_completion_tokens: maxTokens,
|
||||
}
|
||||
|
||||
@@ -274,7 +321,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
@@ -319,7 +366,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
|
||||
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
})
|
||||
|
||||
}
|
||||
@@ -328,7 +375,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
}
|
||||
else {
|
||||
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
|
||||
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||
}
|
||||
@@ -394,8 +441,8 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
const anthropicTools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
|
||||
@@ -405,20 +452,10 @@ const anthropicTools = (chatMode: ChatMode) => {
|
||||
return anthropicTools
|
||||
}
|
||||
|
||||
const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
||||
const { id, name, input } = toolBlock
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
}
|
||||
|
||||
|
||||
// ------------ ANTHROPIC ------------
|
||||
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
|
||||
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode, mcpTools }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
@@ -435,7 +472,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel })
|
||||
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
|
||||
const potentialTools = anthropicTools(chatMode, mcpTools)
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ?
|
||||
{ tools: potentialTools, tool_choice: { type: 'auto' } } as const
|
||||
: {}
|
||||
@@ -459,7 +496,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
@@ -476,7 +513,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
onText({
|
||||
fullText,
|
||||
fullReasoning,
|
||||
toolCall: isAToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
|
||||
toolCall: { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
|
||||
})
|
||||
}
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
@@ -526,7 +563,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||
stream.on('finalMessage', (response) => {
|
||||
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
|
||||
const tools = response.content.filter(c => c.type === 'tool_use')
|
||||
const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0])
|
||||
const toolCall = tools[0] && rawToolCallObjOfAnthropicParams(tools[0])
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
||||
})
|
||||
@@ -642,27 +679,26 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
|
||||
|
||||
// ---------------- GEMINI NATIVE IMPLEMENTATION ----------------
|
||||
|
||||
|
||||
|
||||
const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params } = toolInfo
|
||||
const paramsWithType: { [k: string]: FunctionDeclarationSchemaProperty } = {}
|
||||
for (const key in params) {
|
||||
paramsWithType[key] = { type: SchemaType.STRING, ...params[key] }
|
||||
}
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
parameters: {
|
||||
type: SchemaType.OBJECT,
|
||||
properties: paramsWithType,
|
||||
type: Type.OBJECT,
|
||||
properties: Object.entries(params).reduce((acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
type: Type.STRING,
|
||||
description: value.description
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<string, Schema>)
|
||||
}
|
||||
} satisfies FunctionDeclaration
|
||||
}
|
||||
|
||||
|
||||
const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
const geminiTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined): GeminiTool[] | null => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
const functionDecls: FunctionDeclaration[] = []
|
||||
for (const t in allowedTools ?? {}) {
|
||||
@@ -688,6 +724,7 @@ const sendGeminiChat = async ({
|
||||
providerName,
|
||||
modelSelectionOptions,
|
||||
chatMode,
|
||||
mcpTools,
|
||||
}: SendChatParams_Internal) => {
|
||||
|
||||
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
|
||||
@@ -700,31 +737,31 @@ const sendGeminiChat = async ({
|
||||
// reasoningCapabilities,
|
||||
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
|
||||
|
||||
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
|
||||
// const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
|
||||
|
||||
// reasoning
|
||||
// const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
// const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
const thinkingConfig: ThinkingConfig | undefined = !reasoningInfo?.isReasoningEnabled ? undefined
|
||||
: reasoningInfo.type === 'budget_slider_value' ?
|
||||
{ thinkingBudget: reasoningInfo.reasoningBudget }
|
||||
: undefined
|
||||
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? geminiTools(chatMode) : null
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'gemini-style' ?
|
||||
{ tools: potentialTools } as const
|
||||
: {}
|
||||
const potentialTools = geminiTools(chatMode, mcpTools)
|
||||
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
|
||||
potentialTools
|
||||
: undefined
|
||||
|
||||
// instance
|
||||
const genAI = new GoogleGenerativeAI(
|
||||
thisConfig.apiKey
|
||||
);
|
||||
const model = genAI.getGenerativeModel({
|
||||
systemInstruction: separateSystemMessage,
|
||||
model: modelName,
|
||||
});
|
||||
const genAI = new GoogleGenAI({ apiKey: thisConfig.apiKey });
|
||||
|
||||
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
@@ -735,28 +772,34 @@ const sendGeminiChat = async ({
|
||||
|
||||
let toolName = ''
|
||||
let toolParamsStr = ''
|
||||
let toolId = ''
|
||||
|
||||
model.generateContentStream({
|
||||
systemInstruction: separateSystemMessage ?? undefined,
|
||||
contents: messages as any,
|
||||
...includeInPayload,
|
||||
...nativeToolsObj,
|
||||
|
||||
genAI.models.generateContentStream({
|
||||
model: modelName,
|
||||
config: {
|
||||
systemInstruction: separateSystemMessage,
|
||||
thinkingConfig: thinkingConfig,
|
||||
tools: toolConfig,
|
||||
},
|
||||
contents: messages as GeminiLLMChatMessage[],
|
||||
})
|
||||
.then(async ({ stream, response }) => {
|
||||
.then(async (stream) => {
|
||||
_setAborter(() => { stream.return(fullTextSoFar); });
|
||||
|
||||
// Process the stream
|
||||
for await (const chunk of stream) {
|
||||
// message
|
||||
const newText = chunk.text() ?? ''
|
||||
const newText = chunk.text ?? ''
|
||||
fullTextSoFar += newText
|
||||
|
||||
// tool call
|
||||
const functionCalls = chunk.functionCalls()
|
||||
const functionCalls = chunk.functionCalls
|
||||
if (functionCalls && functionCalls.length > 0) {
|
||||
const functionCall = functionCalls[0] // Get the first function call
|
||||
toolName = functionCall.name ?? ''
|
||||
toolParamsStr = JSON.stringify(functionCall.args ?? {})
|
||||
toolId = functionCall.id ?? ''
|
||||
}
|
||||
|
||||
// (do not handle reasoning yet)
|
||||
@@ -765,7 +808,7 @@ const sendGeminiChat = async ({
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
|
||||
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -773,8 +816,8 @@ const sendGeminiChat = async ({
|
||||
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
} else {
|
||||
const toolId = generateUuid() // gemini does not generate tool IDs. Generate one
|
||||
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
|
||||
if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id
|
||||
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||
}
|
||||
@@ -886,6 +929,12 @@ export const sendLLMMessageToProviderImplementation = {
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
awsBedrock: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
|
||||
} satisfies CallFnOfProvider
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const sendLLMMessage = async ({
|
||||
overridesOfModel,
|
||||
chatMode,
|
||||
separateSystemMessage,
|
||||
mcpTools,
|
||||
}: SendLLMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
@@ -107,7 +108,7 @@ export const sendLLMMessage = async ({
|
||||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
|
||||
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode, mcpTools })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// registered in app.ts
|
||||
// can't make a service responsible for this, because it needs
|
||||
// to be connected to the main process and node dependencies
|
||||
|
||||
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, RawMCPToolCall, MCPToolErrorResponse, MCPServerEventResponse, MCPToolCallParams } from '../common/mcpServiceTypes.js';
|
||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { MCPUserStateOfName } from '../common/voidSettingsTypes.js';
|
||||
|
||||
const getClientConfig = (serverName: string) => {
|
||||
return {
|
||||
name: `${serverName}-client`,
|
||||
version: '0.1.0',
|
||||
// debug: true,
|
||||
}
|
||||
}
|
||||
|
||||
type MCPServerNonError = MCPServer & { status: Omit<MCPServer['status'], 'error'> }
|
||||
type MCPServerError = MCPServer & { status: 'error' }
|
||||
|
||||
|
||||
|
||||
type ClientInfo = {
|
||||
_client: Client, // _client is the client that connects with an mcp client. We're calling mcp clients "server" everywhere except here for naming consistency.
|
||||
mcpServerEntryJSON: MCPConfigFileEntryJSON,
|
||||
mcpServer: MCPServerNonError,
|
||||
} | {
|
||||
_client?: undefined,
|
||||
mcpServerEntryJSON: MCPConfigFileEntryJSON,
|
||||
mcpServer: MCPServerError,
|
||||
}
|
||||
|
||||
type InfoOfClientId = {
|
||||
[clientId: string]: ClientInfo
|
||||
}
|
||||
|
||||
export class MCPChannel implements IServerChannel {
|
||||
|
||||
private readonly infoOfClientId: InfoOfClientId = {}
|
||||
private readonly _refreshingServerNames: Set<string> = new Set()
|
||||
|
||||
// mcp emitters
|
||||
private readonly mcpEmitters = {
|
||||
serverEvent: {
|
||||
onAdd: new Emitter<MCPServerEventResponse>(),
|
||||
onUpdate: new Emitter<MCPServerEventResponse>(),
|
||||
onDelete: new Emitter<MCPServerEventResponse>(),
|
||||
}
|
||||
} satisfies {
|
||||
serverEvent: {
|
||||
onAdd: Emitter<MCPServerEventResponse>,
|
||||
onUpdate: Emitter<MCPServerEventResponse>,
|
||||
onDelete: Emitter<MCPServerEventResponse>,
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
) { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
|
||||
// server events
|
||||
if (event === 'onAdd_server') return this.mcpEmitters.serverEvent.onAdd.event;
|
||||
else if (event === 'onUpdate_server') return this.mcpEmitters.serverEvent.onUpdate.event;
|
||||
else if (event === 'onDelete_server') return this.mcpEmitters.serverEvent.onDelete.event;
|
||||
// else if (event === 'onLoading_server') return this.mcpEmitters.serverEvent.onChangeLoading.event;
|
||||
|
||||
// tool call events
|
||||
|
||||
// handle unknown events
|
||||
else throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
// browser uses this to call (see this.channel.call() in mcpConfigService.ts for all usages)
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
try {
|
||||
if (command === 'refreshMCPServers') {
|
||||
await this._refreshMCPServers(params)
|
||||
}
|
||||
else if (command === 'closeAllMCPServers') {
|
||||
await this._closeAllMCPServers()
|
||||
}
|
||||
else if (command === 'toggleMCPServer') {
|
||||
await this._toggleMCPServer(params.serverName, params.isOn)
|
||||
}
|
||||
else if (command === 'callTool') {
|
||||
const p: MCPToolCallParams = params
|
||||
const response = await this._safeCallTool(p.serverName, p.toolName, p.params)
|
||||
return response
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('mcp channel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// server functions
|
||||
|
||||
|
||||
private async _refreshMCPServers(params: { mcpConfigFileJSON: MCPConfigFileJSON, userStateOfName: MCPUserStateOfName, addedServerNames: string[], removedServerNames: string[], updatedServerNames: string[] }) {
|
||||
|
||||
const {
|
||||
mcpConfigFileJSON,
|
||||
userStateOfName,
|
||||
addedServerNames,
|
||||
removedServerNames,
|
||||
updatedServerNames,
|
||||
} = params
|
||||
|
||||
const { mcpServers: mcpServersJSON } = mcpConfigFileJSON
|
||||
|
||||
const allChanges: { type: 'added' | 'removed' | 'updated', serverName: string }[] = [
|
||||
...addedServerNames.map(n => ({ serverName: n, type: 'added' }) as const),
|
||||
...removedServerNames.map(n => ({ serverName: n, type: 'removed' }) as const),
|
||||
...updatedServerNames.map(n => ({ serverName: n, type: 'updated' }) as const),
|
||||
]
|
||||
|
||||
await Promise.all(
|
||||
allChanges.map(async ({ serverName, type }) => {
|
||||
|
||||
// check if already refreshing
|
||||
if (this._refreshingServerNames.has(serverName)) return
|
||||
this._refreshingServerNames.add(serverName)
|
||||
|
||||
const prevServer = this.infoOfClientId[serverName]?.mcpServer;
|
||||
|
||||
// close and delete the old client
|
||||
if (type === 'removed' || type === 'updated') {
|
||||
await this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]
|
||||
this.mcpEmitters.serverEvent.onDelete.fire({ response: { prevServer, name: serverName, } })
|
||||
}
|
||||
|
||||
// create a new client
|
||||
if (type === 'added' || type === 'updated') {
|
||||
const clientInfo = await this._createClient(mcpServersJSON[serverName], serverName, userStateOfName[serverName]?.isOn)
|
||||
this.infoOfClientId[serverName] = clientInfo
|
||||
this.mcpEmitters.serverEvent.onAdd.fire({ response: { newServer: clientInfo.mcpServer, name: serverName, } })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
allChanges.forEach(({ serverName, type }) => {
|
||||
this._refreshingServerNames.delete(serverName)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private async _createClientUnsafe(server: MCPConfigFileEntryJSON, serverName: string, isOn: boolean): Promise<ClientInfo> {
|
||||
|
||||
const clientConfig = getClientConfig(serverName)
|
||||
const client = new Client(clientConfig)
|
||||
let transport: Transport;
|
||||
let info: MCPServerNonError;
|
||||
|
||||
if (server.url) {
|
||||
// first try HTTP, fall back to SSE
|
||||
try {
|
||||
transport = new StreamableHTTPClientTransport(server.url);
|
||||
await client.connect(transport);
|
||||
console.log(`Connected via HTTP to ${serverName}`);
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: server.url.toString(),
|
||||
}
|
||||
} catch (httpErr) {
|
||||
console.warn(`HTTP failed for ${serverName}, trying SSE…`, httpErr);
|
||||
transport = new SSEClientTransport(server.url);
|
||||
await client.connect(transport);
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
console.log(`Connected via SSE to ${serverName}`);
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: server.url.toString(),
|
||||
}
|
||||
}
|
||||
} else if (server.command) {
|
||||
console.log('ENV DATA: ', server.env)
|
||||
transport = new StdioClientTransport({
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: {
|
||||
...server.env,
|
||||
...process.env
|
||||
} as Record<string, string>,
|
||||
});
|
||||
|
||||
await client.connect(transport)
|
||||
|
||||
// Get the tools from the server
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
|
||||
// Create a full command string for display
|
||||
const fullCommand = `${server.command} ${server.args?.join(' ') || ''}`
|
||||
|
||||
// Format server object
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: fullCommand,
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`No url or command for server ${serverName}`);
|
||||
}
|
||||
|
||||
|
||||
return { _client: client, mcpServerEntryJSON: server, mcpServer: info }
|
||||
}
|
||||
|
||||
private _addUniquePrefix(base: string) {
|
||||
return `${Math.random().toString(36).slice(2, 8)}_${base}`;
|
||||
}
|
||||
|
||||
private async _createClient(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true): Promise<ClientInfo> {
|
||||
try {
|
||||
const c: ClientInfo = await this._createClientUnsafe(serverConfig, serverName, isOn)
|
||||
return c
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to connect to server "${serverName}":`, err)
|
||||
const fullCommand = !serverConfig.command ? '' : `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}`
|
||||
const c: MCPServerError = { status: 'error', error: err + '', command: fullCommand, }
|
||||
return { mcpServerEntryJSON: serverConfig, mcpServer: c, }
|
||||
}
|
||||
}
|
||||
|
||||
private async _closeAllMCPServers() {
|
||||
for (const serverName in this.infoOfClientId) {
|
||||
await this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]
|
||||
}
|
||||
console.log('Closed all MCP servers');
|
||||
}
|
||||
|
||||
private async _closeClient(serverName: string) {
|
||||
const info = this.infoOfClientId[serverName]
|
||||
if (!info) return
|
||||
const { _client: client } = info
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
console.log(`Closed MCP server ${serverName}`);
|
||||
}
|
||||
|
||||
|
||||
private async _toggleMCPServer(serverName: string, isOn: boolean) {
|
||||
const prevServer = this.infoOfClientId[serverName]?.mcpServer
|
||||
// Handle turning on the server
|
||||
if (isOn) {
|
||||
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
|
||||
const clientInfo = await this._createClientUnsafe(this.infoOfClientId[serverName].mcpServerEntryJSON, serverName, isOn)
|
||||
this.mcpEmitters.serverEvent.onUpdate.fire({
|
||||
response: {
|
||||
name: serverName,
|
||||
newServer: clientInfo.mcpServer,
|
||||
prevServer: prevServer,
|
||||
}
|
||||
})
|
||||
}
|
||||
// Handle turning off the server
|
||||
else {
|
||||
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
|
||||
this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]._client
|
||||
|
||||
this.mcpEmitters.serverEvent.onUpdate.fire({
|
||||
response: {
|
||||
name: serverName,
|
||||
newServer: {
|
||||
status: 'offline',
|
||||
tools: [],
|
||||
command: '',
|
||||
// Explicitly set error to undefined to reset the error state
|
||||
error: undefined,
|
||||
},
|
||||
prevServer: prevServer,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tool call functions
|
||||
|
||||
private async _callTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
|
||||
const server = this.infoOfClientId[serverName]
|
||||
if (!server) throw new Error(`Server ${serverName} not found`)
|
||||
const { _client: client } = server
|
||||
if (!client) throw new Error(`Client for server ${serverName} not found`)
|
||||
|
||||
// Call the tool with the provided parameters
|
||||
const response = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: params
|
||||
})
|
||||
const { content } = response as CallToolResult
|
||||
const returnValue = content[0]
|
||||
|
||||
if (returnValue.type === 'text') {
|
||||
// handle text response
|
||||
|
||||
if (response.isError) {
|
||||
throw new Error(`Tool call error: ${response.content}`)
|
||||
// handle error
|
||||
}
|
||||
|
||||
// handle success
|
||||
return {
|
||||
event: 'text',
|
||||
text: returnValue.text,
|
||||
toolName,
|
||||
serverName,
|
||||
}
|
||||
}
|
||||
|
||||
// if (returnValue.type === 'audio') {
|
||||
// // handle audio response
|
||||
// }
|
||||
|
||||
// if (returnValue.type === 'image') {
|
||||
// // handle image response
|
||||
// }
|
||||
|
||||
// if (returnValue.type === 'resource') {
|
||||
// // handle resource response
|
||||
// }
|
||||
|
||||
throw new Error(`Tool call error: We don\'t support ${returnValue.type} tool response yet for tool ${toolName} on server ${serverName}`)
|
||||
}
|
||||
|
||||
// tool call error wrapper
|
||||
private async _safeCallTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
|
||||
try {
|
||||
const response = await this._callTool(serverName, toolName, params)
|
||||
return response
|
||||
} catch (err) {
|
||||
let errorMessage: string;
|
||||
|
||||
// Check if it's an MCP error with a code
|
||||
if (err && typeof err === 'object' && 'code' in err) {
|
||||
const errorCode = err.code;
|
||||
const errorName = err.name || 'Unknown Error';
|
||||
const errorMsg = err.message || '';
|
||||
|
||||
// Map common JSON-RPC error codes to user-friendly messages
|
||||
let codeDescription = '';
|
||||
switch (errorCode) {
|
||||
case -32700:
|
||||
codeDescription = 'Parse Error';
|
||||
break;
|
||||
case -32600:
|
||||
codeDescription = 'Invalid Request';
|
||||
break;
|
||||
case -32601:
|
||||
codeDescription = 'Method Not Found';
|
||||
break;
|
||||
case -32602:
|
||||
codeDescription = 'Invalid Parameters';
|
||||
break;
|
||||
case -32603:
|
||||
codeDescription = 'Internal Error';
|
||||
break;
|
||||
default:
|
||||
codeDescription = `Error Code ${errorCode}`;
|
||||
}
|
||||
|
||||
errorMessage = `${errorName} (${codeDescription})${errorMsg ? ': ' + errorMsg : ''}`;
|
||||
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||
// Standard error with message
|
||||
errorMessage = err.message;
|
||||
} else if (typeof err === 'string') {
|
||||
// String error
|
||||
errorMessage = err;
|
||||
} else {
|
||||
// Unknown error format
|
||||
errorMessage = JSON.stringify(err, null, 2);
|
||||
}
|
||||
|
||||
const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${errorMessage}`;
|
||||
const errorResponse: MCPToolErrorResponse = {
|
||||
event: 'error',
|
||||
text: fullErrorMessage,
|
||||
toolName,
|
||||
serverName,
|
||||
}
|
||||
return errorResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user