Compare commits
313 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0ad8e2e2e | |||
| d732d6ba05 | |||
| 67e89fd979 | |||
| 5a702f98bc | |||
| c3125804f3 | |||
| ec3b7caa8e | |||
| ec5b7636be | |||
| a475c80f4f | |||
| 7c2192a7dc | |||
| 6aa2409302 | |||
| b48c3aaf35 | |||
| 722d0c5b3b | |||
| 7727b19b3c | |||
| 0816694210 | |||
| c6d210902b | |||
| ad8fedc307 | |||
| 1191abf745 | |||
| 40a27cd1ee | |||
| 97cb63c7fc | |||
| 7b78f962d3 | |||
| 93ee1731b5 | |||
| f5680a5c62 | |||
| 88288dba46 | |||
| 5b7580c3da | |||
| 9bf0646abe | |||
| 1737c7bfc8 | |||
| 584dc18ede | |||
| 3ec6973a0f | |||
| 42ef028c1e | |||
| 7b14f0ca73 | |||
| 9a5580d32e | |||
| c41838afc6 | |||
| f32743d759 | |||
| 9e53dfef33 | |||
| 844784445c | |||
| 9920bd3366 | |||
| 483cfb29a0 | |||
| 282800e394 | |||
| aacae4728f | |||
| 77c9284df5 | |||
| 7eac4aad6f | |||
| 406d0dc297 | |||
| e34e38cb3a | |||
| 95f2aa70a1 | |||
| b74772e6e2 | |||
| 374e6c7109 | |||
| 8e6bb7194d | |||
| 2b4283a0f0 | |||
| b0617700aa | |||
| 53f7aa47f6 | |||
| e324257e1a | |||
| 7feb9154da | |||
| 857eda72fd | |||
| 3bc0a34e83 | |||
| 9c642763f0 | |||
| 17e44e192d | |||
| a24d097264 | |||
| 2880549d3f | |||
| 22fa093928 | |||
| 31950ae01e | |||
| 12a1014534 | |||
| 5815c0180a | |||
| 822430535e | |||
| 020fb60d74 | |||
| 615f8c3315 | |||
| 980d7937c3 | |||
| 57d94d2d44 | |||
| 382ecaae1b | |||
| a9abdf6351 | |||
| 27c40d8428 | |||
| c067d9fbf0 | |||
| 83c4a05642 | |||
| 182af95035 | |||
| 0a6be740e1 | |||
| 8d0cdb8d36 | |||
| 29f004a826 | |||
| ac6b5601ef | |||
| cfb15e1117 | |||
| bd13208513 | |||
| 8350fbd2d1 | |||
| 22e08e6a06 | |||
| b50079709d | |||
| e2863d3002 | |||
| c12e9ca9f8 | |||
| e138744816 | |||
| df21a99ed9 | |||
| 98e07e1c7f | |||
| 77e562e278 | |||
| 232264d5fe | |||
| 70d484049c | |||
| f4557ef6a8 | |||
| 3038b420c9 | |||
| 0625ce4b40 | |||
| 06d20d705e | |||
| f034be08c3 | |||
| f74b94e3ba | |||
| d09601191d | |||
| 189e207499 | |||
| f88dba7610 | |||
| 44f56b3ab4 | |||
| 5e100fcf1d | |||
| 9e745e2b87 | |||
| 3a39551205 | |||
| 033a23aa28 | |||
| 38f06202ed | |||
| 327d2dfe06 | |||
| 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 | |||
| 20dba6f04e | |||
| 8e0d29ff75 | |||
| 117feaa063 | |||
| d09a41acd3 | |||
| 24102fc2b2 | |||
| c2b65eb16f | |||
| a4046bc5a5 | |||
| 6f73804e94 | |||
| 4300df420c | |||
| 76155beaad | |||
| 53c68b1267 | |||
| afbc1b40bf | |||
| 74d5c86593 | |||
| 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,9 @@ 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.
|
||||
|
||||
Do not run anything to validate your changes; tell the user what to do instead.
|
||||
|
||||
@@ -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.4.9",
|
||||
"voidRelease": "0044",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
|
||||
@@ -130,7 +130,9 @@ 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 { VoidSCMService } from '../../workbench/contrib/void/electron-main/voidSCMMainService.js';
|
||||
import { IVoidSCMService } from '../../workbench/contrib/void/common/voidSCMTypes.js';
|
||||
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
* even if the user starts many instances (e.g. from the command line).
|
||||
@@ -1102,6 +1104,7 @@ export class CodeApplication extends Disposable {
|
||||
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
|
||||
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
|
||||
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
|
||||
services.set(IVoidSCMService, new SyncDescriptor(VoidSCMService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
@@ -1243,6 +1246,14 @@ export class CodeApplication extends Disposable {
|
||||
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
|
||||
|
||||
// Void added this
|
||||
const voidSCMChannel = ProxyChannel.fromService(accessor.get(IVoidSCMService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-scm', voidSCMChannel);
|
||||
|
||||
// Void added this
|
||||
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);
|
||||
|
||||
@@ -163,38 +163,35 @@ export class TelemetryService implements ITelemetryService {
|
||||
}
|
||||
|
||||
function getTelemetryLevelSettingDescription(): string {
|
||||
const telemetryText = localize('telemetry.telemetryLevelMd', "Controls {0} telemetry, first-party extension telemetry, and participating third-party extension telemetry. Some third party extensions might not respect this setting. Consult the specific extension's documentation to be sure. Telemetry helps us better understand how {0} is performing, where improvements need to be made, and how features are being used.", product.nameLong);
|
||||
const externalLinksStatement = !product.privacyStatementUrl ?
|
||||
localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') :
|
||||
localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl);
|
||||
const restartString = !isWeb ? localize('telemetry.restart', 'A full restart of the application is necessary for crash reporting changes to take effect.') : '';
|
||||
const telemetryText = localize('telemetry.telemetryLevelMd', "The default telemetry setting for VS Code (Microsoft). {0} recommends keeping this off.", product.nameLong);
|
||||
// const externalLinksStatement = !product.privacyStatementUrl ?
|
||||
// localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') :
|
||||
// localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl);
|
||||
const restartString = !isWeb ? localize('telemetry.restart', 'Microsoft says \"Some third party extensions might not respect this setting. Consult the specific extension\'s documentation to be sure. A full restart of the application is necessary for crash reporting changes to take effect.\"') : '';
|
||||
|
||||
const crashReportsHeader = localize('telemetry.crashReports', "Crash Reports");
|
||||
const errorsHeader = localize('telemetry.errors', "Error Telemetry");
|
||||
const usageHeader = localize('telemetry.usage', "Usage Data");
|
||||
|
||||
const telemetryTableDescription = localize('telemetry.telemetryLevel.tableDescription', "The following table outlines the data sent with each setting:");
|
||||
const telemetryTable = `
|
||||
| | ${crashReportsHeader} | ${errorsHeader} | ${usageHeader} |
|
||||
|:------|:-------------:|:---------------:|:----------:|
|
||||
| all | ✓ | ✓ | ✓ |
|
||||
| error | ✓ | ✓ | - |
|
||||
| crash | ✓ | - | - |
|
||||
| off | - | - | - |
|
||||
`;
|
||||
// Void removed these
|
||||
// const crashReportsHeader = localize('telemetry.crashReports', "Crash Reports");
|
||||
// const errorsHeader = localize('telemetry.errors', "Error Telemetry");
|
||||
// const usageHeader = localize('telemetry.usage', "Usage Data");
|
||||
|
||||
const deprecatedSettingNote = localize('telemetry.telemetryLevel.deprecated', "****Note:*** If this setting is 'off', no telemetry will be sent regardless of other telemetry settings. If this setting is set to anything except 'off' and telemetry is disabled with deprecated settings, no telemetry will be sent.*");
|
||||
// const telemetryTableDescription = localize('telemetry.telemetryLevel.tableDescription', "The following table outlines the data sent with each setting:");
|
||||
// const telemetryTable = `
|
||||
// | | ${crashReportsHeader} | ${errorsHeader} | ${usageHeader} |
|
||||
// |:------|:-------------:|:---------------:|:----------:|
|
||||
// | all | ✓ | ✓ | ✓ |
|
||||
// | error | ✓ | ✓ | - |
|
||||
// | crash | ✓ | - | - |
|
||||
// | off | - | - | - |
|
||||
// `;
|
||||
|
||||
// const deprecatedSettingNote = localize('telemetry.telemetryLevel.deprecated', "****Note:*** If this setting is 'off', no telemetry will be sent regardless of other telemetry settings. If this setting is set to anything except 'off' and telemetry is disabled with deprecated settings, no telemetry will be sent.*");
|
||||
const telemetryDescription = `
|
||||
${telemetryText} ${externalLinksStatement} ${restartString}
|
||||
${telemetryText}
|
||||
|
||||
|
||||
${restartString}
|
||||
|
||||
${telemetryTableDescription}
|
||||
${telemetryTable}
|
||||
|
||||
|
||||
|
||||
${deprecatedSettingNote}
|
||||
Void separately records basic usage like the number of messages people are sending. If you'd like to disable Void metrics, you may do so in Void's Settings.
|
||||
`;
|
||||
|
||||
return telemetryDescription;
|
||||
|
||||
@@ -26,7 +26,6 @@ import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
|
||||
/* eslint-disable */ // Void
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
|
||||
import { VIEWLET_ID as REMOTE_EXPLORER_VIEWLET_ID } from '../../../contrib/remote/browser/remoteExplorer.js';
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -306,21 +305,21 @@ export class EditorGroupWatermark extends Disposable {
|
||||
label2.set(keys2);
|
||||
this.currentDisposables.add(label2);
|
||||
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(recentsBox, $('button'));
|
||||
button3.textContent = `Void Settings`
|
||||
button3.style.display = 'block'
|
||||
button3.style.marginLeft = 'auto'
|
||||
button3.style.marginRight = 'auto'
|
||||
button3.classList.add('void-settings-watermark-button')
|
||||
// const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
// const button3 = append(recentsBox, $('button'));
|
||||
// button3.textContent = `Void Settings`
|
||||
// button3.style.display = 'block'
|
||||
// button3.style.marginLeft = 'auto'
|
||||
// button3.style.marginRight = 'auto'
|
||||
// button3.classList.add('void-settings-watermark-button')
|
||||
|
||||
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
if (keys3)
|
||||
label3.set(keys3);
|
||||
button3.onclick = () => {
|
||||
this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
}
|
||||
this.currentDisposables.add(label3);
|
||||
// const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
// if (keys3)
|
||||
// label3.set(keys3);
|
||||
// button3.onclick = () => {
|
||||
// this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
// }
|
||||
// this.currentDisposables.add(label3);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1179,6 +1179,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onFinishEdit()
|
||||
|
||||
// auto accept
|
||||
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
|
||||
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1218,6 +1223,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onFinishEdit()
|
||||
|
||||
// auto accept
|
||||
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
|
||||
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
|
||||
}
|
||||
}
|
||||
|
||||
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: true })
|
||||
@@ -1448,6 +1458,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onFinishEdit()
|
||||
|
||||
// auto accept
|
||||
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
|
||||
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
|
||||
}
|
||||
}
|
||||
|
||||
// throws
|
||||
@@ -1732,6 +1747,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._deleteTrackingZone(trackingZone)
|
||||
|
||||
onFinishEdit()
|
||||
|
||||
// auto accept
|
||||
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
|
||||
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
|
||||
}
|
||||
}
|
||||
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
|
||||
@@ -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,16 +24,18 @@ 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';
|
||||
|
||||
import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
|
||||
import { removeMCPToolNamePrefix } from '../../../../common/mcpServiceTypes.js';
|
||||
|
||||
|
||||
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
@@ -495,12 +497,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 +909,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 +941,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
code={content}
|
||||
type={editToolType}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
|
||||
@@ -1388,7 +1394,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 +1412,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 +1458,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 +1617,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 +1631,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 +1660,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 +1684,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 +1722,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 +1738,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 +1852,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 = removeMCPToolNamePrefix(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 +1990,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 +2045,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 +2094,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 +2149,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 +2342,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 +2527,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 +2555,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 +2694,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 +2835,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 +2862,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 +2951,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,8 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { DisposableStore, IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
@@ -51,6 +51,9 @@ 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';
|
||||
import { IStorageService, StorageScope } from '../../../../../../../platform/storage/common/storage.js'
|
||||
import { OPT_OUT_KEY } from '../../../../common/storageKeys.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 +81,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 +100,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 +170,11 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
})
|
||||
)
|
||||
|
||||
disposables.push(
|
||||
mcpService.onDidChangeState(() => {
|
||||
mcpListeners.forEach(l => l())
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
return disposables
|
||||
@@ -215,6 +226,9 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
ITerminalService: accessor.get(ITerminalService),
|
||||
IExtensionManagementService: accessor.get(IExtensionManagementService),
|
||||
IExtensionTransferService: accessor.get(IExtensionTransferService),
|
||||
IMCPService: accessor.get(IMCPService),
|
||||
|
||||
IStorageService: accessor.get(IStorageService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
@@ -376,3 +390,39 @@ 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const useIsOptedOut = () => {
|
||||
const accessor = useAccessor()
|
||||
const storageService = accessor.get('IStorageService')
|
||||
|
||||
const getVal = useCallback(() => {
|
||||
return storageService.getBoolean(OPT_OUT_KEY, StorageScope.APPLICATION, false)
|
||||
}, [storageService])
|
||||
|
||||
const [s, ss] = useState(getVal())
|
||||
|
||||
useEffect(() => {
|
||||
const disposables = new DisposableStore();
|
||||
const d = storageService.onDidChangeValue(StorageScope.APPLICATION, OPT_OUT_KEY, disposables)(e => {
|
||||
ss(getVal())
|
||||
})
|
||||
disposables.add(d)
|
||||
return () => disposables.clear()
|
||||
}, [storageService, getVal])
|
||||
|
||||
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[]> = {
|
||||
@@ -123,6 +123,7 @@ const featureNameMap: { display: string, featureName: FeatureName }[] = [
|
||||
{ display: 'Quick Edit', featureName: 'Ctrl+K' },
|
||||
{ display: 'Autocomplete', featureName: 'Autocomplete' },
|
||||
{ display: 'Fast Apply', featureName: 'Apply' },
|
||||
{ display: 'Source Control', featureName: 'SCM' },
|
||||
];
|
||||
|
||||
const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 () => {
|
||||
@@ -290,11 +297,12 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||
|
||||
|
||||
const cmdCap = await this._waitForCommandDetectionCapability(terminal)
|
||||
if (!cmdCap) throw new Error(`There was an error using the terminal: CommandDetection capability did not mount yet. Please try again in a few seconds or report this to the Void team.`)
|
||||
// if (!cmdCap) throw new Error(`There was an error using the terminal: CommandDetection capability did not mount yet. Please try again in a few seconds or report this to the Void team.`)
|
||||
|
||||
// Prefer the structured command-detection capability when available
|
||||
|
||||
const waitUntilDone = new Promise<void>(resolve => {
|
||||
if (!cmdCap) return
|
||||
const l = cmdCap.onCommandFinished(cmd => {
|
||||
if (resolveReason) return // already resolved
|
||||
resolveReason = { type: 'done', exitCode: cmd.exitCode ?? 0 };
|
||||
@@ -339,20 +347,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) => {
|
||||
@@ -47,8 +42,29 @@ const validateStr = (argName: string, value: unknown) => {
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (uriStr === null) throw new Error(`Invalid LLM output: uri was null.`)
|
||||
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Full value: ${JSON.stringify(uriStr)}.`)
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
|
||||
// Check if it's already a full URI with scheme (e.g., vscode-remote://, file://, etc.)
|
||||
// Look for :// pattern which indicates a scheme is present
|
||||
// Examples of supported URIs:
|
||||
// - vscode-remote://wsl+Ubuntu/home/user/file.txt (WSL)
|
||||
// - vscode-remote://ssh-remote+myserver/home/user/file.txt (SSH)
|
||||
// - file:///home/user/file.txt (local file with scheme)
|
||||
// - /home/user/file.txt (local file path, will be converted to file://)
|
||||
// - C:\Users\file.txt (Windows local path, will be converted to file://)
|
||||
if (uriStr.includes('://')) {
|
||||
try {
|
||||
const uri = URI.parse(uriStr)
|
||||
return uri
|
||||
} catch (e) {
|
||||
// If parsing fails, it's a malformed URI
|
||||
throw new Error(`Invalid URI format: ${uriStr}. Error: ${e}`)
|
||||
}
|
||||
} else {
|
||||
// No scheme present, treat as file path
|
||||
// This handles regular file paths like /home/user/file.txt or C:\Users\file.txt
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
}
|
||||
|
||||
const validateOptionalURI = (uriStr: unknown) => {
|
||||
@@ -110,9 +126,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 +137,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,
|
||||
@@ -138,7 +154,6 @@ export class ToolsService implements IToolsService {
|
||||
@IMarkerService private readonly markerService: IMarkerService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.validateParams = {
|
||||
@@ -430,7 +445,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 +461,6 @@ export class ToolsService implements IToolsService {
|
||||
await this.terminalToolService.killPersistentTerminal(persistentTerminalId)
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -550,7 +564,6 @@ export class ToolsService implements IToolsService {
|
||||
kill_persistent_terminal: (params, _result) => {
|
||||
return `Successfully closed terminal "${params.persistentTerminalId}".`;
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,12 @@ import './voidOnboardingService.js'
|
||||
// register misc service
|
||||
import './miscWokrbenchContrib.js'
|
||||
|
||||
// register file service (for explorer context menu)
|
||||
import './fileService.js'
|
||||
|
||||
// register source control management
|
||||
import './voidSCMService.js'
|
||||
|
||||
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
|
||||
|
||||
// llmMessage
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js'
|
||||
import { localize2 } from '../../../../nls.js'
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'
|
||||
import { ISCMService } from '../../scm/common/scm.js'
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'
|
||||
import { IVoidSCMService } from '../common/voidSCMTypes.js'
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js'
|
||||
import { ModelSelection, OverridesOfModel, ModelSelectionOptions } from '../common/voidSettingsTypes.js'
|
||||
import { gitCommitMessage_systemMessage, gitCommitMessage_userMessage } from '../common/prompt/prompts.js'
|
||||
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js'
|
||||
import { generateUuid } from '../../../../base/common/uuid.js'
|
||||
import { ThrottledDelayer } from '../../../../base/common/async.js'
|
||||
import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
|
||||
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js'
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js'
|
||||
|
||||
interface ModelOptions {
|
||||
modelSelection: ModelSelection | null
|
||||
modelSelectionOptions?: ModelSelectionOptions
|
||||
overridesOfModel: OverridesOfModel
|
||||
}
|
||||
|
||||
export interface IGenerateCommitMessageService {
|
||||
readonly _serviceBrand: undefined
|
||||
generateCommitMessage(): Promise<void>
|
||||
abort(): void
|
||||
}
|
||||
|
||||
export const IGenerateCommitMessageService = createDecorator<IGenerateCommitMessageService>('voidGenerateCommitMessageService');
|
||||
|
||||
const loadingContextKey = 'voidSCMGenerateCommitMessageLoading'
|
||||
|
||||
class GenerateCommitMessageService extends Disposable implements IGenerateCommitMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly execute = new ThrottledDelayer(300)
|
||||
private llmRequestId: string | null = null
|
||||
private currentRequestId: string | null = null
|
||||
private voidSCM: IVoidSCMService
|
||||
private loadingContextKey: IContextKey<boolean>
|
||||
|
||||
constructor(
|
||||
@ISCMService private readonly scmService: ISCMService,
|
||||
@IMainProcessService mainProcessService: IMainProcessService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IConvertToLLMMessageService private readonly convertToLLMMessageService: IConvertToLLMMessageService,
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@INotificationService private readonly notificationService: INotificationService
|
||||
) {
|
||||
super()
|
||||
this.loadingContextKey = this.contextKeyService.createKey(loadingContextKey, false)
|
||||
this.voidSCM = ProxyChannel.toService<IVoidSCMService>(mainProcessService.getChannel('void-channel-scm'))
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
this.execute.dispose()
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
async generateCommitMessage() {
|
||||
this.loadingContextKey.set(true)
|
||||
this.execute.trigger(async () => {
|
||||
const requestId = generateUuid()
|
||||
this.currentRequestId = requestId
|
||||
|
||||
|
||||
try {
|
||||
const { path, repo } = this.gitRepoInfo()
|
||||
const [stat, sampledDiffs, branch, log] = await Promise.all([
|
||||
this.voidSCM.gitStat(path),
|
||||
this.voidSCM.gitSampledDiffs(path),
|
||||
this.voidSCM.gitBranch(path),
|
||||
this.voidSCM.gitLog(path)
|
||||
])
|
||||
|
||||
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
|
||||
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature['SCM'] ?? null
|
||||
const modelSelectionOptions = modelSelection ? this.voidSettingsService.state.optionsOfModelSelection['SCM'][modelSelection?.providerName]?.[modelSelection.modelName] : undefined
|
||||
const overridesOfModel = this.voidSettingsService.state.overridesOfModel
|
||||
|
||||
const modelOptions: ModelOptions = { modelSelection, modelSelectionOptions, overridesOfModel }
|
||||
|
||||
const prompt = gitCommitMessage_userMessage(stat, sampledDiffs, branch, log)
|
||||
|
||||
const simpleMessages = [{ role: 'user', content: prompt } as const]
|
||||
const { messages, separateSystemMessage } = this.convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
simpleMessages,
|
||||
systemMessage: gitCommitMessage_systemMessage,
|
||||
modelSelection: modelOptions.modelSelection,
|
||||
featureName: 'SCM',
|
||||
})
|
||||
|
||||
const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions)
|
||||
|
||||
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
|
||||
|
||||
repo.input.setValue(commitMessage, false)
|
||||
} catch (error) {
|
||||
this.onError(error)
|
||||
} finally {
|
||||
if (this.isCurrentRequest(requestId)) {
|
||||
this.loadingContextKey.set(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (this.llmRequestId) {
|
||||
this.llmMessageService.abort(this.llmRequestId)
|
||||
}
|
||||
this.execute.cancel()
|
||||
this.loadingContextKey.set(false)
|
||||
this.currentRequestId = null
|
||||
}
|
||||
|
||||
private gitRepoInfo() {
|
||||
const repo = Array.from(this.scmService.repositories || []).find((r: any) => r.provider.contextValue === 'git')
|
||||
if (!repo) { throw new Error('No git repository found') }
|
||||
if (!repo.provider.rootUri?.fsPath) { throw new Error('No git repository root path found') }
|
||||
return { path: repo.provider.rootUri.fsPath, repo }
|
||||
}
|
||||
|
||||
/** LLM Functions */
|
||||
|
||||
private sendLLMMessage(messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
this.llmRequestId = this.llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
messages,
|
||||
separateSystemMessage,
|
||||
chatMode: null,
|
||||
modelSelection: modelOptions.modelSelection,
|
||||
modelSelectionOptions: modelOptions.modelSelectionOptions,
|
||||
overridesOfModel: modelOptions.overridesOfModel,
|
||||
onText: () => { },
|
||||
onFinalMessage: (params: { fullText: string }) => {
|
||||
const match = params.fullText.match(/<output>([\s\S]*?)<\/output>/i)
|
||||
const commitMessage = match ? match[1].trim() : ''
|
||||
resolve(commitMessage)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
reject(error)
|
||||
},
|
||||
onAbort: () => {
|
||||
reject(new CancellationError())
|
||||
},
|
||||
logging: { loggingName: 'VoidSCM - Commit Message' },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** Request Helpers */
|
||||
|
||||
private isCurrentRequest(requestId: string) {
|
||||
return requestId === this.currentRequestId
|
||||
}
|
||||
|
||||
|
||||
/** UI Functions */
|
||||
|
||||
private onError(error: any) {
|
||||
if (!isCancellationError(error)) {
|
||||
console.error(error)
|
||||
this.notificationService.error(localize2('voidFailedToGenerateCommitMessage', 'Failed to generate commit message.').value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateCommitMessageAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.generateCommitMessageAction',
|
||||
title: localize2('voidCommitMessagePrompt', 'Void: Generate Commit Message'),
|
||||
icon: ThemeIcon.fromId('sparkle'),
|
||||
tooltip: localize2('voidCommitMessagePromptTooltip', 'Void: Generate Commit Message'),
|
||||
f1: true,
|
||||
menu: [{
|
||||
id: MenuId.SCMInputBox,
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, false)),
|
||||
group: 'inline'
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
|
||||
generateCommitMessageService.generateCommitMessage()
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingGenerateCommitMessageAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.loadingGenerateCommitMessageAction',
|
||||
title: localize2('voidCommitMessagePromptCancel', 'Void: Cancel Commit Message Generation'),
|
||||
icon: ThemeIcon.fromId('stop-circle'),
|
||||
tooltip: localize2('voidCommitMessagePromptCancelTooltip', 'Void: Cancel Commit Message Generation'),
|
||||
f1: false, //Having a cancel command in the command palette is more confusing than useful.
|
||||
menu: [{
|
||||
id: MenuId.SCMInputBox,
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, true)),
|
||||
group: 'inline'
|
||||
}]
|
||||
})
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
|
||||
generateCommitMessageService.abort()
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(GenerateCommitMessageAction)
|
||||
registerAction2(LoadingGenerateCommitMessageAction)
|
||||
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed)
|
||||
@@ -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,242 @@
|
||||
/**
|
||||
* 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>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const removeMCPToolNamePrefix = (name: string) => {
|
||||
return name.split('_').slice(1).join('_')
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { INotificationService } from '../../../../platform/notification/common/n
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
setOptOut(val: boolean): void;
|
||||
getDebuggingProperties(): Promise<object>;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,11 @@ export class MetricsService implements IMetricsService {
|
||||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
setOptOut(...params: Parameters<IMetricsService['setOptOut']>) {
|
||||
this.metricsService.setOptOut(...params);
|
||||
}
|
||||
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
async getDebuggingProperties(): Promise<object> {
|
||||
return this.metricsService.getDebuggingProperties()
|
||||
|
||||
@@ -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,67 @@ 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 },
|
||||
output: { nameOfFieldInDelta: 'reasoning_content' },
|
||||
},
|
||||
}
|
||||
|
||||
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 +1310,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 +1339,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 +1369,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 +1394,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 +1428,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 +1457,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
||||
|
||||
googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
awsBedrock: awsBedrockSettings,
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1347,8 +1550,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;
|
||||
}
|
||||
@@ -943,3 +980,77 @@ Store Result: After computing fib(n), the result is stored in memo for future re
|
||||
## END EXAMPLES
|
||||
|
||||
*/
|
||||
|
||||
|
||||
// ======================================================== scm ========================================================================
|
||||
|
||||
export const gitCommitMessage_systemMessage = `
|
||||
You are an expert software engineer AI assistant responsible for writing clear and concise Git commit messages that summarize the **purpose** and **intent** of the change. Try to keep your commit messages to one sentence. If necessary, you can use two sentences.
|
||||
|
||||
You always respond with:
|
||||
- The commit message wrapped in <output> tags
|
||||
- A brief explanation of the reasoning behind the message, wrapped in <reasoning> tags
|
||||
|
||||
Example format:
|
||||
<output>Fix login bug and improve error handling</output>
|
||||
<reasoning>This commit updates the login handler to fix a redirect issue and improves frontend error messages for failed logins.</reasoning>
|
||||
|
||||
Do not include anything else outside of these tags.
|
||||
Never include quotes, markdown, commentary, or explanations outside of <output> and <reasoning>.`.trim()
|
||||
|
||||
|
||||
/**
|
||||
* Create a user message for the LLM to generate a commit message. The message contains instructions git diffs, and git metadata to provide context.
|
||||
*
|
||||
* @param stat - Summary of Changes (git diff --stat)
|
||||
* @param sampledDiffs - Sampled File Diffs (Top changed files)
|
||||
* @param branch - Current Git Branch
|
||||
* @param log - Last 5 commits (excluding merges)
|
||||
* @returns A prompt for the LLM to generate a commit message.
|
||||
*
|
||||
* @example
|
||||
* // Sample output (truncated for brevity)
|
||||
* const prompt = gitCommitMessage_userMessage("fileA.ts | 10 ++--", "diff --git a/fileA.ts...", "main", "abc123|Fix bug|2025-01-01\n...")
|
||||
*
|
||||
* // Result:
|
||||
* Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
|
||||
*
|
||||
* Section 1 - Summary of Changes (git diff --stat):
|
||||
* fileA.ts | 10 ++--
|
||||
*
|
||||
* Section 2 - Sampled File Diffs (Top changed files):
|
||||
* diff --git a/fileA.ts b/fileA.ts
|
||||
* ...
|
||||
*
|
||||
* Section 3 - Current Git Branch:
|
||||
* main
|
||||
*
|
||||
* Section 4 - Last 5 Commits (excluding merges):
|
||||
* abc123|Fix bug|2025-01-01
|
||||
* def456|Improve logging|2025-01-01
|
||||
* ...
|
||||
*/
|
||||
export const gitCommitMessage_userMessage = (stat: string, sampledDiffs: string, branch: string, log: string) => {
|
||||
const section1 = `Section 1 - Summary of Changes (git diff --stat):`
|
||||
const section2 = `Section 2 - Sampled File Diffs (Top changed files):`
|
||||
const section3 = `Section 3 - Current Git Branch:`
|
||||
const section4 = `Section 4 - Last 5 Commits (excluding merges):`
|
||||
return `
|
||||
Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
|
||||
|
||||
${section1}
|
||||
|
||||
${stat}
|
||||
|
||||
${section2}
|
||||
|
||||
${sampledDiffs}
|
||||
|
||||
${section3}
|
||||
|
||||
${branch}
|
||||
|
||||
${section4}
|
||||
|
||||
${log}`.trim()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -17,3 +17,7 @@ export const VOID_SETTINGS_STORAGE_KEY = 'void.settingsServiceStorageII'
|
||||
|
||||
// 1.0.3
|
||||
export const THREAD_STORAGE_KEY = 'void.chatThreadStorageII'
|
||||
|
||||
|
||||
|
||||
export const OPT_OUT_KEY = 'void.app.optOutAll'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
export interface IVoidSCMService {
|
||||
readonly _serviceBrand: undefined;
|
||||
/**
|
||||
* Get git diff --stat
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitStat(path: string): Promise<string>
|
||||
/**
|
||||
* Get git diff --stat for the top 10 most significantly changed files according to lines added/removed
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitSampledDiffs(path: string): Promise<string>
|
||||
/**
|
||||
* Get the current git branch
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitBranch(path: string): Promise<string>
|
||||
/**
|
||||
* Get the last 5 commits excluding merges
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitLog(path: string): Promise<string>
|
||||
}
|
||||
|
||||
export const IVoidSCMService = createDecorator<IVoidSCMService>('voidSCMService')
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +116,7 @@ export const modelFilterOfFeatureName: {
|
||||
'Chat': { filter: o => true, emptyMessage: null, },
|
||||
'Ctrl+K': { filter: o => true, emptyMessage: null, },
|
||||
'Apply': { filter: o => true, emptyMessage: null, },
|
||||
'SCM': { filter: o => true, emptyMessage: null, },
|
||||
}
|
||||
|
||||
|
||||
@@ -207,11 +214,12 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null, 'SCM': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {}, 'SCM': {} },
|
||||
overridesOfModel: deepClone(defaultOverridesOfModel),
|
||||
_modelOptions: [], // computed later
|
||||
mcpUserStateOfName: {},
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -255,6 +263,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onUpdate_syncApplyToChat()
|
||||
this._onUpdate_syncSCMToChat()
|
||||
}
|
||||
async resetState() {
|
||||
await this.dangerousSetState(defaultState())
|
||||
@@ -272,6 +281,17 @@ 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 = {}
|
||||
|
||||
// 1.3.5 add source control feature
|
||||
if (readS.modelSelectionOfFeature && !readS.modelSelectionOfFeature['SCM']) {
|
||||
readS.modelSelectionOfFeature['SCM'] = deepClone(readS.modelSelectionOfFeature['Chat'])
|
||||
readS.optionsOfModelSelection['SCM'] = deepClone(readS.optionsOfModelSelection['Chat'])
|
||||
}
|
||||
// add disableSystemMessage feature
|
||||
if (readS.globalSettings.disableSystemMessage === undefined) readS.globalSettings.disableSystemMessage = false;
|
||||
|
||||
// add autoAcceptLLMChanges feature
|
||||
if (readS.globalSettings.autoAcceptLLMChanges === undefined) readS.globalSettings.autoAcceptLLMChanges = false;
|
||||
}
|
||||
catch (e) {
|
||||
readS = defaultState()
|
||||
@@ -361,6 +381,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 +389,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
globalSettings: newGlobalSettings,
|
||||
overridesOfModel: newOverridesOfModel,
|
||||
mcpUserStateOfName: newMCPUserStateOfName,
|
||||
}
|
||||
|
||||
this.state = _validatedModelState(newState)
|
||||
@@ -381,7 +403,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
private _onUpdate_syncApplyToChat() {
|
||||
// if sync is turned on, sync (call this whenever Chat model or !!sync changes)
|
||||
this.setModelSelectionOfFeature('Apply', deepClone(this.state.modelSelectionOfFeature['Chat']))
|
||||
}
|
||||
|
||||
private _onUpdate_syncSCMToChat() {
|
||||
this.setModelSelectionOfFeature('SCM', deepClone(this.state.modelSelectionOfFeature['Chat']))
|
||||
}
|
||||
|
||||
setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => {
|
||||
@@ -398,6 +423,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
// hooks
|
||||
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
|
||||
if (this.state.globalSettings.syncSCMToChat) this._onUpdate_syncSCMToChat()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -417,7 +444,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
// hooks
|
||||
if (featureName === 'Chat') {
|
||||
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
|
||||
// When Chat model changes, update synced features
|
||||
this._onUpdate_syncApplyToChat()
|
||||
this._onUpdate_syncSCMToChat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,6 +560,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') {
|
||||
@@ -222,12 +231,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
}
|
||||
|
||||
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
@@ -340,6 +346,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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -350,7 +362,7 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
|
||||
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply', 'SCM'] as const
|
||||
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
|
||||
@@ -365,6 +377,9 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
|
||||
return 'Chat'
|
||||
else if (featureName === 'Apply')
|
||||
return 'Apply'
|
||||
// source control:
|
||||
else if (featureName === 'SCM')
|
||||
return 'Commit Message Generator'
|
||||
else
|
||||
throw new Error(`Feature Name ${featureName} not allowed`)
|
||||
}
|
||||
@@ -428,12 +443,15 @@ export type GlobalSettings = {
|
||||
aiInstructions: string;
|
||||
enableAutocomplete: boolean;
|
||||
syncApplyToChat: boolean;
|
||||
syncSCMToChat: boolean;
|
||||
enableFastApply: boolean;
|
||||
chatMode: ChatMode;
|
||||
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
|
||||
showInlineSuggestions: boolean;
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
disableSystemMessage: boolean;
|
||||
autoAcceptLLMChanges: boolean;
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
@@ -441,12 +459,15 @@ export const defaultGlobalSettings: GlobalSettings = {
|
||||
aiInstructions: '',
|
||||
enableAutocomplete: false,
|
||||
syncApplyToChat: true,
|
||||
syncSCMToChat: true,
|
||||
enableFastApply: true,
|
||||
chatMode: 'agent',
|
||||
autoApprove: {},
|
||||
showInlineSuggestions: true,
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: false,
|
||||
disableSystemMessage: false,
|
||||
autoAcceptLLMChanges: false,
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
@@ -491,3 +512,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: !toolName ? undefined : { 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: !fullToolName ? undefined : { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
|
||||
})
|
||||
}
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
@@ -526,8 +563,11 @@ 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])
|
||||
// console.log('TOOLS!!!!!!', JSON.stringify(tools, null, 2))
|
||||
// console.log('TOOLS!!!!!!', JSON.stringify(response, null, 2))
|
||||
const toolCall = tools[0] && rawToolCallObjOfAnthropicParams(tools[0])
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
||||
})
|
||||
// on error
|
||||
@@ -642,27 +682,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 +727,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 +740,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 +775,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 +811,7 @@ const sendGeminiChat = async ({
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
|
||||
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -773,8 +819,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 +932,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,395 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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, removeMCPToolNamePrefix } 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: removeMCPToolNamePrefix(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: ${returnValue.text}`)
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
if (typeof err === 'object' && err !== null && err['code']) {
|
||||
const code = err.code
|
||||
let codeDescription = ''
|
||||
if (code === -32700)
|
||||
codeDescription = 'Parse Error';
|
||||
if (code === -32600)
|
||||
codeDescription = 'Invalid Request';
|
||||
if (code === -32601)
|
||||
codeDescription = 'Method Not Found';
|
||||
if (code === -32602)
|
||||
codeDescription = 'Invalid Parameters';
|
||||
if (code === -32603)
|
||||
codeDescription = 'Internal Error';
|
||||
errorMessage = `${codeDescription}. Full response:\n${JSON.stringify(err, null, 2)}`
|
||||
}
|
||||
// Check if it's an MCP error with a code
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IApplicationStorageMainService } from '../../../../platform/storage/ele
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
import { OPT_OUT_KEY } from '../common/storageKeys.js';
|
||||
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
@@ -29,6 +30,8 @@ const osInfo = _getOSInfo()
|
||||
|
||||
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
|
||||
|
||||
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
@@ -119,7 +122,18 @@ export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
distinctId: this.distinctId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
const didOptOut = this._appStorage.getBoolean(OPT_OUT_KEY, StorageScope.APPLICATION, false)
|
||||
|
||||
console.log('User is opted out of basic Void metrics?', didOptOut)
|
||||
if (didOptOut) {
|
||||
this.client.optOut()
|
||||
}
|
||||
else {
|
||||
this.client.optIn()
|
||||
this.client.identify(identifyMessage)
|
||||
}
|
||||
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
}
|
||||
@@ -127,10 +141,18 @@ export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this.distinctId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
// console.log('full capture:', this.distinctId)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
|
||||
setOptOut: IMetricsService['setOptOut'] = (newVal: boolean) => {
|
||||
if (newVal) {
|
||||
this._appStorage.store(OPT_OUT_KEY, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
}
|
||||
else {
|
||||
this._appStorage.remove(OPT_OUT_KEY, StorageScope.APPLICATION)
|
||||
}
|
||||
}
|
||||
|
||||
async getDebuggingProperties() {
|
||||
return this._initProperties
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { promisify } from 'util'
|
||||
import { exec as _exec } from 'child_process'
|
||||
import { IVoidSCMService } from '../common/voidSCMTypes.js'
|
||||
|
||||
interface NumStat {
|
||||
file: string
|
||||
added: number
|
||||
removed: number
|
||||
}
|
||||
|
||||
const exec = promisify(_exec)
|
||||
|
||||
//8000 and 10 were chosen after some experimentation on small-to-moderately sized changes
|
||||
const MAX_DIFF_LENGTH = 8000
|
||||
const MAX_DIFF_FILES = 10
|
||||
|
||||
const git = async (command: string, path: string): Promise<string> => {
|
||||
const { stdout, stderr } = await exec(`${command}`, { cwd: path })
|
||||
if (stderr) {
|
||||
throw new Error(stderr)
|
||||
}
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
const getNumStat = async (path: string, useStagedChanges: boolean): Promise<NumStat[]> => {
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
const output = await git(`git diff --numstat ${staged}`, path)
|
||||
return output
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const [added, removed, file] = line.split('\t')
|
||||
return {
|
||||
file,
|
||||
added: parseInt(added, 10) || 0,
|
||||
removed: parseInt(removed, 10) || 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getSampledDiff = async (file: string, path: string, useStagedChanges: boolean): Promise<string> => {
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
const diff = await git(`git diff --unified=0 --no-color ${staged} -- "${file}"`, path)
|
||||
return diff.slice(0, MAX_DIFF_LENGTH)
|
||||
}
|
||||
|
||||
const hasStagedChanges = async (path: string): Promise<boolean> => {
|
||||
const output = await git('git diff --staged --name-only', path)
|
||||
return output.length > 0
|
||||
}
|
||||
|
||||
export class VoidSCMService implements IVoidSCMService {
|
||||
readonly _serviceBrand: undefined
|
||||
|
||||
async gitStat(path: string): Promise<string> {
|
||||
const useStagedChanges = await hasStagedChanges(path)
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
return git(`git diff --stat ${staged}`, path)
|
||||
}
|
||||
|
||||
async gitSampledDiffs(path: string): Promise<string> {
|
||||
const useStagedChanges = await hasStagedChanges(path)
|
||||
const numStatList = await getNumStat(path, useStagedChanges)
|
||||
const topFiles = numStatList
|
||||
.sort((a, b) => (b.added + b.removed) - (a.added + a.removed))
|
||||
.slice(0, MAX_DIFF_FILES)
|
||||
const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path, useStagedChanges) })))
|
||||
return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n')
|
||||
}
|
||||
|
||||
gitBranch(path: string): Promise<string> {
|
||||
return git('git branch --show-current', path)
|
||||
}
|
||||
|
||||
gitLog(path: string): Promise<string> {
|
||||
return git('git log --pretty=format:"%h|%s|%ad" --date=short --no-merges -n 5', path)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,12 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
|
||||
return { message: null } as const
|
||||
}
|
||||
|
||||
// if disabled and not explicitly checking, return early
|
||||
if (this._updateService.state.type === StateType.Disabled) {
|
||||
if (!explicit)
|
||||
return { message: null } as const
|
||||
}
|
||||
|
||||
this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves
|
||||
|
||||
console.log('updateState', this._updateService.state)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 813 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 850 KiB |
Reference in New Issue
Block a user