Compare commits

...

181 Commits

Author SHA1 Message Date
Andrew Pareles a0fa104d23 Merge pull request #401 from voideditor/model-selection
Model selection
2025-04-15 02:34:33 -07:00
Andrew Pareles 7fd0b133e5 misc prompt improvements 2025-04-15 02:30:37 -07:00
Andrew Pareles 321876932a loading 2025-04-15 01:51:59 -07:00
Andrew Pareles eda8517c78 Merge pull request #400 from voideditor/model-selection
GPT 4.1 and Claude 3.7 Thinking with Tools
2025-04-14 23:52:58 -07:00
Andrew Pareles 6ce37f7627 misc 2025-04-14 23:47:14 -07:00
Andrew Pareles 18b76885f5 1.2.1 2025-04-14 23:17:33 -07:00
Andrew Pareles ba8644fbb6 read_file improvements 2025-04-14 23:12:10 -07:00
Andrew Pareles ee959ded08 4.1 and 3.7 work! 2025-04-14 22:57:29 -07:00
Mathew Pareles 499a47904e style 2025-04-14 22:53:39 -07:00
Mathew Pareles 5ac03bb917 fix 2025-04-14 22:47:10 -07:00
Mathew Pareles 922f3ba2d7 fix current file 2025-04-14 22:46:54 -07:00
Mathew Pareles a189629341 fix current file 2025-04-14 22:46:15 -07:00
Andrew Pareles d2098f71eb support 4.1 native tools and 3.7 native thinking... 2025-04-14 22:01:46 -07:00
Mathew Pareles 77c59f2e80 lint error severity must be warning or error 2025-04-14 14:41:48 -07:00
Mathew Pareles e6ecad3f65 desc 2025-04-14 14:34:04 -07:00
Mathew Pareles ee43ec99ea duplicate tooltips 2025-04-14 13:59:32 -07:00
Andrew Pareles e77039e9ad Merge pull request #399 from voideditor/model-selection
1.2.0
2025-04-14 13:38:12 -07:00
Andrew Pareles 6aa1375067 1.2.0 2025-04-14 13:36:43 -07:00
Andrew Pareles fe017948c6 Merge pull request #398 from voideditor/model-selection
GPT 4.1 + fixes
2025-04-14 13:34:33 -07:00
Andrew Pareles 9b3d4cbdba lint errors 2025-04-14 13:33:49 -07:00
Andrew Pareles a8cf0b361f prompt 2025-04-14 13:29:13 -07:00
Andrew Pareles 680125baec hot-swap out default models on mount 2025-04-14 12:48:44 -07:00
Andrew Pareles 0db4f7118d Merge remote-tracking branch 'origin/main' into model-selection 2025-04-14 12:31:49 -07:00
Andrew Pareles 2ea20243a8 lint error UI + misc 2025-04-14 12:31:31 -07:00
Andrew Pareles 4d02381dab Merge pull request #321 from jcommaret/feat_mistral-199
[Provider]Mistral -😻- Mistral working : [OpenAICompatibleModel Chat + tools +  sendMistralFIM]
2025-04-14 12:03:53 -07:00
Andrew Pareles 6990216fbe api 2025-04-14 12:03:37 -07:00
Andrew Pareles f5d68d9348 add back comment 2025-04-14 12:02:48 -07:00
Andrew Pareles 165c5aac24 fixes 2025-04-14 11:59:22 -07:00
Andrew Pareles 22f4a75c00 Merge branch 'main' into pr/jcommaret/321 2025-04-14 11:59:14 -07:00
Andrew Pareles 06513168c1 mistral corrections 2025-04-14 11:51:42 -07:00
Andrew Pareles a80883c10f 4.1 fallback for OSS providers 2025-04-14 11:10:52 -07:00
Andrew Pareles 20b0fd6af9 add 4.1 2025-04-14 10:57:26 -07:00
Andrew Pareles 5171f7244f Merge pull request #397 from voideditor/model-selection
Misc
2025-04-14 01:28:56 -07:00
Andrew Pareles 43708d25e7 1.1.0 2025-04-14 01:28:37 -07:00
Andrew Pareles 2b54733242 disable onboarding 2025-04-13 22:07:30 -07:00
Andrew Pareles cd77d1a1c4 Update README.md 2025-04-13 20:38:46 -07:00
Andrew Pareles 37af09add8 message 2025-04-13 12:04:17 -07:00
Andrew Pareles ade1e70536 Merge pull request #395 from voideditor/model-selection
minor fixes
2025-04-12 20:16:39 -07:00
Andrew Pareles 159e0b677f minor fixes 2025-04-12 20:16:20 -07:00
Andrew Pareles 335ebe1e11 Merge pull request #394 from voideditor/model-selection
Merge remote-tracking branch 'origin/main' into model-selection
2025-04-12 14:08:52 -07:00
Andrew Pareles 7be1ae5003 Merge remote-tracking branch 'origin/main' into model-selection 2025-04-12 14:07:28 -07:00
Andrew Pareles 5d7f8fc91f Merge pull request #393 from voideditor/model-selection
Quasar + misc fixes before 1.0.3
2025-04-12 14:07:11 -07:00
Andrew Pareles d6b3bdee35 interrupt fallback 2025-04-12 14:06:05 -07:00
Andrew Pareles 8c1626f79b quasar 2025-04-12 14:06:05 -07:00
Andrew Pareles 97ffe0b4cc read lint errors tool 2025-04-12 14:06:05 -07:00
Mathew Pareles 9cf316f808 tooltip 2025-04-12 13:55:46 -07:00
Mathew Pareles aabacba2a9 messaging 2025-04-12 13:52:28 -07:00
Mathew Pareles 39ccb1bbbd msg 2025-04-12 13:51:01 -07:00
Mathew Pareles 63560308b5 lint errors more manageable 2025-04-12 13:48:12 -07:00
Mathew Pareles 967aff2d19 placeholder for tools 2025-04-12 13:26:23 -07:00
Mathew Pareles 50327a238e add onboarding working draft (need to add images + enable but basically done) 2025-04-12 13:22:13 -07:00
Andrew Pareles 8eb5abe429 Merge pull request #392 from voideditor/model-selection
prompt
2025-04-11 14:31:56 -07:00
Andrew Pareles 3c532478de prompt 2025-04-11 14:31:10 -07:00
Andrew Pareles 5d5839736e Merge pull request #391 from voideditor/model-selection
Prepare for 1.0.3 release
2025-04-11 14:26:19 -07:00
Andrew Pareles b8fd40e735 enable auto updates! 2025-04-11 14:25:40 -07:00
Andrew Pareles a869ddb6f3 rm tools from capabilities 2025-04-11 13:22:33 -07:00
Andrew Pareles e5a40e3d35 edit streams in 2025-04-11 13:17:31 -07:00
Andrew Pareles 1081b8879f Merge pull request #389 from voideditor/model-selection
Misc 1.0.3 canary improvements - tool call fix, tooltips, lint error passing
2025-04-11 03:34:29 -07:00
Andrew Pareles 4efc5209ab fix 2025-04-11 03:34:15 -07:00
Andrew Pareles adcdeed094 Merge remote-tracking branch 'origin/main' into model-selection 2025-04-11 03:33:30 -07:00
Andrew Pareles 86285e2393 dirStr limits 2025-04-11 03:31:18 -07:00
Andrew Pareles c1b6b2e01d tooltips 2025-04-11 03:31:18 -07:00
Andrew Pareles 2dcdd78316 fix extractGrammar and lots of misc improvements 2025-04-11 03:31:18 -07:00
Andrew Pareles fa9a3db86f fix extractCode 2025-04-11 03:31:18 -07:00
Mathew Pareles 4445ff28ce missing comment 2025-04-10 01:18:25 -07:00
Mathew Pareles 9d6a4c4648 ux 2025-04-10 01:09:51 -07:00
Mathew Pareles e41f8790b5 gradient 2025-04-10 00:07:14 -07:00
Andrew Pareles d363edb1a8 Update VOID_CODEBASE_GUIDE.md 2025-04-10 00:02:24 -07:00
Mathew Pareles 3cddc5d5cc spacing 2025-04-09 23:56:25 -07:00
Mathew Pareles 2509290ab2 spacing 2025-04-09 23:55:43 -07:00
Mathew Pareles aba49edfdd add a model back 2025-04-09 23:40:58 -07:00
Mathew Pareles 13b60f7ee3 auxbar size 2025-04-09 23:25:41 -07:00
Mathew Pareles bd5cf464c2 lint error ui 2025-04-09 22:33:17 -07:00
Andrew Pareles 44e7d10ece Update HOW_TO_CONTRIBUTE.md 2025-04-09 21:51:07 -07:00
Andrew Pareles 6bf79b27c0 Dummy service update (#387)
* +

* +

* +

* +
2025-04-09 21:49:07 -07:00
Andrew Pareles bb22aefb6a Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:39:23 -07:00
Andrew Pareles 7002f320d6 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:35:53 -07:00
Andrew Pareles def8803919 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:33:30 -07:00
Andrew Pareles fdb21a68b8 Update README.md 2025-04-09 21:32:54 -07:00
Andrew Pareles 455acc3490 Update README.md 2025-04-09 21:32:38 -07:00
Andrew Pareles bc5b07b26f Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:31:10 -07:00
Andrew Pareles 79ad57b112 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:30:59 -07:00
Andrew Pareles 9391dcb0b8 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:29:01 -07:00
Andrew Pareles b90716e38e Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:25:06 -07:00
Andrew Pareles d8d21b0f25 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:24:53 -07:00
Andrew Pareles 3bf06bfe26 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:24:15 -07:00
Andrew Pareles 9166fa8684 Update README.md 2025-04-09 21:23:29 -07:00
Andrew Pareles df2165403e Update README.md 2025-04-09 21:23:19 -07:00
Andrew Pareles 44e0f0dfc7 Update README.md 2025-04-09 21:22:55 -07:00
Andrew Pareles d6e496c396 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:21:20 -07:00
Andrew Pareles 7c94b95df4 Update README.md 2025-04-09 21:18:35 -07:00
Andrew Pareles 85759eeb4a Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:17:15 -07:00
Andrew Pareles 6230971dff Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:16:33 -07:00
Andrew Pareles 8f08e40819 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:15:52 -07:00
Andrew Pareles ae635f0102 Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:15:29 -07:00
Andrew Pareles cb8cc587f6 + 2025-04-09 21:13:25 -07:00
Andrew Pareles 77f931c923 + 2025-04-09 21:12:10 -07:00
Andrew Pareles 8dcb5d35df Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:11:46 -07:00
Andrew Pareles 6483dcffa8 + 2025-04-09 21:11:40 -07:00
Andrew Pareles ccaf6e2e3d Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:10:57 -07:00
Andrew Pareles d5ae1a328c Update VOID_CODEBASE_GUIDE.md 2025-04-09 21:10:24 -07:00
Andrew Pareles 45c83ee661 + 2025-04-09 20:58:41 -07:00
Andrew Pareles af7dbb4b70 Update HOW_TO_CONTRIBUTE.md 2025-04-09 20:53:52 -07:00
Andrew Pareles 72a6daaa58 Update VOID_CODEBASE_GUIDE.md 2025-04-09 20:52:24 -07:00
Andrew Pareles 898a5803dd Update VOID_CODEBASE_GUIDE.md 2025-04-09 20:45:49 -07:00
Andrew Pareles cffc6f6db9 Update VOID_CODEBASE_GUIDE.md 2025-04-09 20:45:36 -07:00
Andrew Pareles b2ba634cc4 Update HOW_TO_CONTRIBUTE.md 2025-04-09 20:05:55 -07:00
Andrew Pareles c62d6a6790 Update HOW_TO_CONTRIBUTE.md 2025-04-09 20:05:17 -07:00
Andrew Pareles f8bee1d4a2 Update HOW_TO_CONTRIBUTE.md 2025-04-09 20:05:01 -07:00
Andrew Pareles 41e66a73c4 Update and rename VOID_USEFUL_LINKS.md to VOID_CODEBASE_GUIDE.md 2025-04-09 20:01:55 -07:00
Andrew Pareles 5da0e4821f Merge pull request #386 from voideditor/model-selection
HOW_TO_CONTRIBUTE
2025-04-09 18:46:47 -07:00
Andrew Pareles 6aefdc3095 HOW_TO_CONTRIBUTE 2025-04-09 18:46:25 -07:00
Andrew Pareles 4055dc7775 log 2025-04-09 17:08:05 -07:00
Andrew Pareles f48889627a Merge pull request #385 from voideditor/model-selection
logs
2025-04-09 17:02:12 -07:00
Andrew Pareles 3c59428fe8 logs 2025-04-09 17:01:42 -07:00
Andrew Pareles 8a8a982d21 Merge pull request #384 from voideditor/model-selection
Model selection
2025-04-09 16:57:59 -07:00
Andrew Pareles 7f890b946d add logs 2025-04-09 16:57:35 -07:00
Andrew Pareles 1799ae9d84 Merge remote-tracking branch 'origin/main' into model-selection 2025-04-09 16:47:28 -07:00
Andrew Pareles c7bdd2d64c copilot head 2025-04-09 16:22:38 -07:00
Andrew Pareles 5fde9194af rm voidimage bug 2025-04-09 08:45:38 -07:00
Andrew Pareles b6408512d3 Merge pull request #381 from voideditor/tools-in-plaintext
Increased Support for Agent Mode (Ollama, vLLM, etc)
2025-04-09 08:29:29 -07:00
Jérôme Commaret 5e83d419ce Merge Main and add English Comments 2025-04-09 13:47:33 +02:00
Jérôme Commaret b1626849ba Merge branch 'main' into feat_mistral-199 2025-04-09 13:33:06 +02:00
Jérôme Commaret 2f078326c2 Merge pull request #5 from jcommaret/dev
Dev
2025-04-08 19:48:22 +02:00
Jérôme Commaret 448be9a2cb Met à jour les dépendances dans package-lock.json, y compris la mise à niveau de plusieurs paquets vers leurs dernières versions, ajout de licences manquantes et suppression de dépendances obsolètes. 2025-04-08 19:18:29 +02:00
Jérôme Commaret 0a1f675f45 Merge branch 'main' into dev 2025-04-08 19:17:25 +02:00
Jérôme Commaret 795ce62ce8 fix mistral tooling 2025-04-03 20:55:30 +02:00
Jérôme Commaret 2d9fe3b0be fix models capabilities 2025-04-02 23:39:44 +02:00
Jérôme Commaret 03992c1206 Merge branch 'voideditor:main' into main 2025-03-31 16:48:53 +02:00
Jérôme Commaret 0c30c2681d Merge branch 'voideditor:main' into main 2025-03-30 00:23:24 +01:00
Jérôme Commaret 4f183f6772 Merge remote-tracking branch 'origin/feat_close-update-notification' 2025-03-27 20:43:22 +01:00
Jérôme Commaret f61a7c91e4 Merge remote-tracking branch 'origin/feat_mistral-199' 2025-03-27 20:25:38 +01:00
Jérôme Commaret 610009eb1c Merge branch 'voideditor:main' into main 2025-03-27 20:07:15 +01:00
Jérôme Commaret cd1b783917 Merge branch 'main' into feat_mistral-199 2025-03-27 13:46:55 +01:00
Jérôme Commaret 015a7c10dc fix : notification closed, but appearing again 2025-03-25 15:50:33 +01:00
Jérôme Commaret f0c8eb55a6 fix close button 2025-03-25 14:37:06 +01:00
Jérôme Commaret e0aee9863e ADD : closing the notification update 2025-03-24 16:26:16 +01:00
Jérôme Commaret 4cc7f292b0 Merge branch 'main' into feat_mistral-199 2025-03-22 16:54:11 +01:00
Jérôme Commaret 7c991b4dfc Merge branch 'voideditor:main' into main 2025-03-22 12:29:55 +01:00
Jérôme Commaret 84d17737cf Merge branch 'main' into feat_mistral-199 2025-03-22 12:24:48 +01:00
Jérôme Commaret 4c6b32633b Merge branch 'voideditor:main' into main 2025-03-22 12:22:32 +01:00
Andrew Pareles b933c5cf45 Merge pull request #326 from voideditor/model-selection
Small (or empty) file editing
2025-03-22 12:22:32 +01:00
Andrew Pareles 5b76b4a6a3 Merge pull request #325 from voideditor/v1.0.2
V1.0.2
2025-03-22 12:22:32 +01:00
Mathew Pareles 251d175517 fixes 2025-03-22 12:22:32 +01:00
Andrew Pareles 69ce84c8df Merge pull request #324 from voideditor/model-selection
Command Bar for Changes + Tool UI
2025-03-22 12:22:32 +01:00
Andrew Pareles f9e9771a87 No X button 2025-03-22 12:22:32 +01:00
Mathew Pareles 18ff819ae9 fix search+replace on empty files 2025-03-22 12:22:32 +01:00
Andrew Pareles 98c09c653a 1 2025-03-22 12:22:32 +01:00
Andrew Pareles e1d4d85846 more persistent update message 2025-03-22 12:22:32 +01:00
Andrew Pareles e52e55a431 rm 2025-03-22 12:22:32 +01:00
Andrew Pareles 32d465dbe8 window 2025-03-22 12:22:32 +01:00
Andrew Pareles 1f9bc92b66 1.0.2 2025-03-22 12:22:32 +01:00
Andrew Pareles 9e5c82d6f8 current file fix will vs did 2025-03-22 12:22:32 +01:00
Andrew Pareles dd429208ce void agent 2025-03-22 12:22:32 +01:00
Andrew Pareles 82278ab115 format 2025-03-22 12:22:32 +01:00
Andrew Pareles 13cb8d4de7 misc improvements 2025-03-22 12:22:32 +01:00
Mathew Pareles 2bc004f54d ux for commandbar 2025-03-22 12:22:32 +01:00
Andrew Pareles e58b6041b4 Merge remote-tracking branch 'origin/main' into model-selection 2025-03-22 12:22:32 +01:00
Jérôme Commaret f7d4eebf56 Add Mistral 2025-03-22 03:14:06 +01:00
Jérôme Commaret 8e735ec453 Fix model 2025-03-22 03:00:55 +01:00
Jérôme Commaret 850bca80ab Merge branch 'feat_mistral-199' into dev-jco 2025-03-22 01:00:08 +01:00
Jérôme Commaret 440dd809e3 Merge branch 'dev' into dev-jco 2025-03-22 00:59:27 +01:00
Jérôme Commaret ffb416e4e8 Merge remote-tracking branch 'upstream/model-selection' into dev 2025-03-22 00:56:58 +01:00
Jérôme Commaret 0a17a9de19 Merge branch 'main' into dev 2025-03-22 00:47:40 +01:00
Jérôme Commaret a0a7900381 Merge branch 'voideditor:main' into main 2025-03-22 00:45:06 +01:00
Jérôme Commaret 5f77b9faae Réducing Mistral Model context lenght 2025-03-22 00:28:13 +01:00
Jérôme Commaret 3fc97c1a83 Mistral model update 2025-03-21 17:51:05 +01:00
Jérôme Commaret a3a8a7a842 Merge branch 'feat_bundle_speed' into dev-jco 2025-03-19 12:51:19 +01:00
Jérôme Commaret e28449b83d Bump: voidVersion 2025-03-19 12:36:09 +01:00
Jérôme Commaret 6ecc5cdbad fix voidversion 2025-03-19 12:35:55 +01:00
Jérôme Commaret d64b6fba31 Merge branch 'dev' into dev-jco 2025-03-19 12:29:14 +01:00
Jérôme Commaret d7ac86a6bd Merge branch 'main' into dev 2025-03-19 12:28:29 +01:00
Jérôme Commaret 598f0d47f8 Merge branch 'feat_mistral-199' into feat_bundle_speed 2025-03-19 11:59:56 +01:00
Jérôme Commaret 3b1ccdd2b1 Merge branch 'feat_bundle_speed' into dev-jco 2025-03-19 11:03:23 +01:00
Jérôme Commaret 054a8ad85a Fix bundle speed on local computer 2025-03-19 11:01:21 +01:00
Jérôme Commaret 31ae3ea46e Merge branch 'feat_mistral-199' into dev-jco 2025-03-18 03:22:44 +01:00
Jérôme Commaret 67d453cffe mistralModelOptions is still to update 2025-03-18 03:01:48 +01:00
Jérôme Commaret ab557d4707 Merge branch 'voidVersion-from-tag' into dev-jco 2025-03-17 22:43:00 +01:00
Jérôme Commaret e5a21bf7c6 Merge branch 'feat_mistral-199' into dev-jco 2025-03-17 22:42:30 +01:00
Jérôme Commaret d5ef3b7d9b Merge branch 'voideditor:main' into main 2025-03-17 18:32:05 +01:00
Jérôme Commaret f3753c8da0 Merge branch 'main' into feat_mistral-199 2025-03-17 18:26:29 +01:00
48 changed files with 3257 additions and 2279 deletions
+2 -4
View File
@@ -12,11 +12,9 @@ There are a few ways to contribute:
### Codebase Guide
We [highly recommend reading this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization too. Void's codebase is pretty simple when you know what a service is and what `browser/` and `common/` mean, and the article covers all the jargon.
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute!
<!-- ADD BLOG HERE
We wrote a [guide to working in VSCode].
-->
The repo is not as intimidating as it first seems if you read the guide!
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
+4 -3
View File
@@ -15,25 +15,26 @@ This repo contains the full sourcecode for Void. We are currently in [open beta]
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
- 📝 [Changelog](https://voideditor.com/changelog)
- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md)
## Contributing
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
2. To get started working on Void, see [here](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
3. We're open to collaborations and suggestions of all types - just reach out.
## Reference
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [our Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
## Support
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
+162
View File
@@ -0,0 +1,162 @@
# Void Codebase Guide
The Void codebase is not as intimidating as it seems!
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
The purpose of this document is to explain how Void's codebase works. If you want build instructions, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
## Void Codebase Guide
### Terminology
Here is some important terminology you should know if you're working inside VSCode:
- An **Editor** is the thing that you type your code in. If you have 10 tabs open, that's just one editor! Editors contain tabs (or "models").
- A **Model** is an internal representation of a file's contents. It's shared between editors (for example, if you press `Cmd+\` to make a new editor, then the model of a file like `A.ts` is shared between them. Two editors, one model. That's how changes sync.).
- Each model has a **URI** it represents, like `/Users/.../my_file.txt`. (A URI or "resource" is generally just a path).
- The **Workbench** is the wrapper that contains all the editors, the terminal, the file system tree, etc.
- Usually you use the `ITextModel` type for models and the `ICodeEditor` type for editors. There aren't that many other types.
### Minimal VSCode Rundown
Here's a minimal VSCode rundown if you're just getting started with Void:
- VSCode is (and therefore Void is) an Electron app. Electron runs two processes: a **main** process (for internals) and a **browser** process (browser means HTML in general, not just "web browser").
- Code in a `browser/` folder always lives on the browser process, and it can use `window` and other browser items.
- Code in an `electron-main/` folder always lives on the main process, and it can import `node_modules`.
- Code in `common/` can be used by either process, but doesn't get any special imports.
- The browser environment is not allowed to import `node_modules`, but there are two workarounds:
1. Bundle the raw node_module code to the browser - we're doing this for React.
2. Implement the code on `electron-main/` and set up a channel between main/browser - we're doing this for sendLLMMessage.
VSCode is organized into "Services". A service is just a class that mounts a single time (in computer science theory this is called a "singleton"). You can register services with `registerSingleton` so that you can easily use them in any constructor with `@<Service>`. See _dummyContrib for an example we put together on how to register them. The registration is the same every time.
Services are always lazily created, even if you register them as Eager. If you want something that always runs on Void's mount, you should use a "workbench contribution". See _dummyContrib for this. Very similar to a Service, just registered slightly differently.
Actions or "commands" are functions you register on VSCode so that either you or the user can call them later. You can run actions as a user by pressing Cmd+Shift+P (opens the command pallete), or you can run them internally by using the commandService to call them by ID. We use actions to register keybinding listeners like Cmd+L, Cmd+K, etc. The nice thing about actions is the user can change the keybindings.
See [here](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for a decent VSCode guide with even more info.
Each section below contains an overview of a core part of Void's sourcecode. You might want to scroll to find the item that's relevant to you.
### Internal LLM Message Pipeline
Here's a picture of all the dependencies that are relevent between the time you first send a message through Void's sidebar, and the time a request is sent to your provider.
Sending LLM messages from the main process avoids CSP issues with local providers and lets us use node_modules more easily.
<div align="center">
<img width="100%" src="https://github.com/user-attachments/assets/9cf54dbb-82c4-4488-97a2-bd8dea890b50">
</div>
**Notes:** `modelCapabilities` is an important file that must be updated when new models come out!
### Apply
Void has two types of Apply: **Fast Apply** (uses Search/Replace, see below), and **Slow Apply** (rewrites whole file).
When you click Apply and Fast Apply is enabled, we prompt the LLM to output Search/Replace block(s) like this:
```
<<<<<<< ORIGINAL
// original code goes here
=======
// replaced code goes here
>>>>>>> UPDATED
```
This is what allows Void to quickly apply code even on 1000-line files. It's the same as asking the LLM to press Ctrl+F and enter in a search/replace query.
### Apply Inner Workings
The `editCodeService` file runs Apply. The same exact code is also used when the LLM calls the Edit tool, and when you submit Cmd+K. Just different versions of Fast/Slow Apply mode.
Here is some important terminology:
- A **DiffZone** is a {startLine, endLine} region in which we show Diffs (red/green areas). We update it when the user types, so it's always accurate.
- A **DiffArea** is a generalization that tracks line numbers like a DiffZone.
- The only type of zone that can "stream" is a DiffZone. Each DiffZone has an llmCancelToken if it's streaming.
- When you click Apply, we create a **DiffZone** over that the full file so that any changes that the LLM makes will show up in red/green. We then stream the change.
- When an LLM calls Edit, it's really calling Apply.
- When you submit Cmd+K, it's the same as Apply except we create a smaller DiffZone (not on the whole file).
### Writing Files Inner Workings
When Void wants to change your code, it just writes to a text model. This means all you need to know to write to a file is its URI - you don't have to load it, save it, etc. There are some annoying background URI/model things to think about to get this to work, but we handled them all in `voidModelService`.
### Void Settings Inner Workings
We have a service `voidSettingsService` that stores all your Void settings (providers, models, global Void settings, etc). Imagine this as an implicit dependency for any of the core Void services:
<div align="center">
<img width="800" src="https://github.com/user-attachments/assets/9f3cb68c-a61b-4810-8429-bb90b992b3fa">
</div>
Here's a guide to some of the terminology we're using:
- **FeatureName**: Autocomplete | Chat | CtrlK | Apply
- **ModelSelection**: a {providerName, modelName} pair.
- **ProviderName**: The name of a provider: `'ollama'`, `'openAI'`, etc.
- **ModelName**: The name of a model (string type, eg `'gpt-4o'`).
- **RefreshProvider**: a provider that we ping repeatedly to update the models list.
- **ChatMode** = normal | gather | agent
### Approval State
`editCodeService`'s data structures contain all the information about changes that the user needs to review. However, they don't store that information in a useful format. We wrote the following service to get a more useful derived state:
<div align="center">
<img width="600" src="https://github.com/user-attachments/assets/f3645355-dff6-467c-bc38-ffe52077c08b">
</div>
### Build process
If you want to know how our build pipeline works, see our build repo [here](https://github.com/voideditor/void-builder).
## VSCode Codebase Guide (Not Void)
The Void team put together this list of links to get up and running with VSCode's sourcecode, the foundation of Void. We hope it's helpful!
#### Links for Beginners
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
#### Links for Contributors
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
#### Misc
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
- Note: VSCode's repo is the source code for the Monaco editor! An "editor" is a Monaco editor, and it shares the code for ITextModel, etc.
#### VSCode's Extension API
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
-39
View File
@@ -1,39 +0,0 @@
# Useful links
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
For a complete guide on building Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
## Contributing
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
## Beginners / Getting started
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
## Misc
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
## VSCode's Extension API
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
+1 -1
View File
@@ -1,7 +1,7 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.0.3",
"voidVersion": "1.2.1",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
@@ -52,7 +52,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState';
// Use the side bar dimensions
override readonly minimumWidth: number = 230; // Void changed this (was 170)
override readonly minimumWidth: number = 280; // Void changed this (was 170)
override readonly maximumWidth: number = Number.POSITIVE_INFINITY;
override readonly minimumHeight: number = 0;
override readonly maximumHeight: number = Number.POSITIVE_INFINITY;
@@ -654,10 +654,12 @@ const defaultChat = {
submenu: MenuId.ChatTitleBarMenu,
title: localize('title4', "Copilot"),
icon: Codicon.copilot,
when: ContextKeyExpr.and(
ChatContextKeys.supported,
ContextKeyExpr.has('config.chat.commandCenter.enabled')
),
// Void commented this out - copilot head
when: ContextKeyExpr.false(),
// when: ContextKeyExpr.and(
// ChatContextKeys.supported,
// ContextKeyExpr.has('config.chat.commandCenter.enabled')
// ),
order: 10001 // to the right of command center
});
@@ -667,11 +669,13 @@ MenuRegistry.appendMenuItem(MenuId.TitleBar, {
title: localize('title4', "Copilot"),
group: 'navigation',
icon: Codicon.copilot,
when: ContextKeyExpr.and(
ChatContextKeys.supported,
ContextKeyExpr.has('config.chat.commandCenter.enabled'),
ContextKeyExpr.has('config.window.commandCenter').negate(),
),
when: ContextKeyExpr.false(),
// Void commented this out - copilot head
// when: ContextKeyExpr.and(
// ChatContextKeys.supported,
// ContextKeyExpr.has('config.chat.commandCenter.enabled'),
// ContextKeyExpr.has('config.window.commandCenter').negate(),
// ),
order: 1
}); */
@@ -6,6 +6,7 @@
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { localize2 } from '../../../../nls.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
@@ -14,15 +15,16 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
// to change this, just Cmd+Shift+F and replace DummyService with YourServiceName, and create a unique ID below
export interface IDummyService {
readonly _serviceBrand: undefined;
readonly _serviceBrand: undefined; // services need this, just leave it undefined
}
export const IDummyService = createDecorator<IDummyService>('DummyService');
// An example of an action (delete if you're not using an action):
registerAction2(class extends Action2 {
constructor() {
super({
@@ -42,18 +44,21 @@ registerAction2(class extends Action2 {
}
})
// on mount
class DummyService extends Disposable implements IWorkbenchContribution, IDummyService {
static readonly ID = 'workbench.contrib.void.dummy'
static readonly ID = 'workbench.contrib.void.dummy' // workbenchContributions need this, services do not
_serviceBrand: undefined;
constructor(
@ICodeEditorService codeEditorService: ICodeEditorService,
) {
super()
}
}
// pick one and delete the other:
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
@@ -34,7 +34,7 @@
// // const result = await new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search_files'],
// // tools: ['search_for_files'],
// // onFinalMessage: ({ result: r, }) => {
// // res(r)
// // },
@@ -73,7 +73,7 @@
// // const result = new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search_files'],
// // tools: ['search_for_files'],
// // onResult: (r) => {
// // res(r)
// // }
@@ -20,6 +20,7 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { isWindows } from '../../../../base/common/platform.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
// import { IContextGatheringService } from './contextGatheringService.js';
@@ -791,18 +792,21 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
const featureName: FeatureName = 'Autocomplete'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const aiInstructions = this._settingsService.state.globalSettings.aiInstructions
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
messagesType: 'FIMMessage',
messages: {
prefix: llmPrefix,
suffix: llmSuffix,
stopTokens: stopTokens,
},
messages: this._convertToLLMMessageService.prepareFIMMessage({
messages: {
prefix: llmPrefix,
suffix: llmSuffix,
stopTokens: stopTokens,
},
aiInstructions
}),
modelSelection,
modelSelectionOptions,
logging: { loggingName: 'Autocomplete' },
@@ -890,6 +894,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
) {
super()
@@ -11,11 +11,10 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, chat_systemMessage, ToolName, toolCallXMLStr, } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.js';
@@ -23,7 +22,6 @@ import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolMessage } from '../common/chatThreadServiceTypes.js';
import { Position } from '../../../../editor/common/core/position.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IMetricsService } from '../common/metricsService.js';
import { shorten } from '../../../../base/common/labels.js';
import { IVoidModelService } from '../common/voidModelService.js';
@@ -33,11 +31,35 @@ import { findLast, findLastIdx } from '../../../../base/common/arraysFind.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { truncate } from '../../../../base/common/strings.js';
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
if (!currentSelections) return null
for (let i = 0; i < currentSelections.length; i += 1) {
const s = currentSelections[i]
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
if (s.type === 'File' && newSelection.type === 'File') {
return i
}
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
// if there's any collision return true
const [oldStart, oldEnd] = s.range
const [newStart, newEnd] = newSelection.range
if (oldStart !== newStart || oldEnd !== newEnd) continue
return i
}
if (s.type === 'Folder' && newSelection.type === 'Folder') {
return i
}
}
return null
}
/*
@@ -221,17 +243,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@IVoidModelService private readonly _voidModelService: IVoidModelService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IToolsService private readonly _toolsService: IToolsService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@ITerminalToolService private readonly _terminalToolService: ITerminalToolService,
@IMetricsService private readonly _metricsService: IMetricsService,
@IEditorService private readonly _editorService: IEditorService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
@INotificationService private readonly _notificationService: INotificationService,
@IModelService private readonly _modelService: IModelService,
@IDirectoryStrService private readonly _directoryStrService: IDirectoryStrService,
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
@@ -288,14 +307,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
const oldStagingSelections = this.getCurrentThreadState().stagingSelections || [];
const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath)
if (fileIsAlreadyHere) return
// remove all old selectons that are marked as `wasAddedAsCurrentFile`, and add new selection
const newStagingSelections: StagingSelectionItem[] = [
...oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile),
newStagingSelection
]
// remove all old selectons that are marked as `wasAddedAsCurrentFile`
const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => s.state && !s.state.wasAddedAsCurrentFile)
const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath)
if (!fileIsAlreadyHere) {
newStagingSelections.push(newStagingSelection)
}
this.setCurrentThreadState({ stagingSelections: newStagingSelections });
}
@@ -454,10 +475,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
else return
const { name } = lastMsg
const { name, id, rawParams } = lastMsg
const errorMessage = this.errMsgs.rejected
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null })
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
this._setStreamState(threadId, {}, 'set')
}
@@ -482,7 +503,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) {
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
@@ -505,69 +526,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {}
// system message
private _generateSystemMessage = async (chatMode: ChatMode) => {
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
const activeURI = this._editorService.activeEditor?.resource?.fsPath;
const directoryStr = await this._directoryStrService.getAllDirectoriesStr({
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ? `...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const runningTerminalIds = this._terminalToolService.listTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode })
return systemMessage
}
private _generateLLMMessages = async (threadId: string) => {
const thread = this.state.allThreads[threadId]
if (!thread) return []
const chatMessages = deepClone(thread.messages)
const llmChatMessages: LLMChatMessage[] = []
// merge tools into user message
for (const c of chatMessages) {
if (c.role === 'assistant') {
// if called a tool, re-add its XML to the message
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content = c.displayContent
if (c.toolCall) {
content = `${content}\n\n${toolCallXMLStr(c.toolCall)}`
}
llmChatMessages.push({ role: c.role, content: content, anthropicReasoning: c.anthropicReasoning })
}
else if (c.role === 'user' || c.role === 'tool') {
if (c.role === 'tool')
c.content = `<${c.name}_result>\n${c.content}\n</${c.name}_result>`
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
llmChatMessages.push({ role: 'user', content: c.content })
else
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
}
else if (c.role === 'interrupted_streaming_tool') { // pass
}
else if (c.role === 'checkpoint') { // pass
}
else {
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}
return llmChatMessages
}
// returns true when the tool call is waiting for user approval
private _runToolCall = async (
threadId: string,
toolName: ToolName,
opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
toolId: string,
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
// compute these below
@@ -582,7 +546,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolParams = params
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, })
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, })
return {}
}
// once validated, add checkpoint for edit
@@ -593,7 +557,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (toolRequiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams })
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
@@ -603,9 +567,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolParams = opts.validatedParams
}
// 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null })
this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams })
let interrupted = false
try {
@@ -616,14 +582,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
delete this._currentlyRunningToolInterruptor[threadId];
}
toolResult = await result // ts is bad... await is needed
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
}
catch (error) {
if (interrupted) {
// the tool result is added when we stop running
return { interrupted: true }
}
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, })
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
@@ -632,12 +599,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
// 5. add to history and keep going
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
};
@@ -658,6 +625,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' }
}) {
// above just defines helpers, below starts the actual function
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
@@ -671,7 +639,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params })
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
if (interrupted) return
}
@@ -687,34 +655,36 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// send llm message
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
const systemMessage = await this._generateSystemMessage(chatMode)
const llmMessages = await this._generateLLMMessages(threadId)
const messages: LLMChatMessage[] = [
{ role: 'system', content: systemMessage },
...llmMessages
]
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
chatMessages,
modelSelection,
chatMode
})
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
messages,
messages: messages,
modelSelection,
modelSelectionOptions,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
separateSystemMessage: separateSystemMessage,
onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise()
},
@@ -741,7 +711,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// call tool if there is one
const tool: RawToolCallObj | undefined = toolCall
if (tool) {
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams })
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams })
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
// just detect tool interruption which is the same as chat interruption right now
@@ -954,7 +924,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const [_, toIdx] = c
if (toIdx === fromIdx) return
console.log(`going from ${fromIdx} to ${toIdx}`)
// console.log(`going from ${fromIdx} to ${toIdx}`)
// update the user's checkpoint
this._addUserModificationsToCurrCheckpoint({ threadId })
@@ -1049,6 +1019,7 @@ We only need to do it for files that were edited since `from`, ie files between
severity: error ? Severity.Warning : Severity.Info,
message: error ? `Error: ${error} ` : `A new Chat result is ready.`,
source: messageContent,
sticky: true,
actions: {
primary: [{
id: 'void.goToChat',
@@ -1485,9 +1456,9 @@ We only need to do it for files that were edited since `from`, ie files between
}
}, true)
// when change focused message idx, jump
if (messageIdx !== undefined)
this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
// // when change focused message idx, jump - do not jump back when click edit, too confusing.
// if (messageIdx !== undefined)
// this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
}
// set message.state
@@ -0,0 +1,607 @@
import { Disposable } from '../../../../base/common/lifecycle.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
import { toolCallXMLStr, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { ITerminalToolService } from './terminalToolService.js';
type SimpleLLMMessage = {
role: 'tool';
content: string;
id: string;
name: ToolName;
rawParams: RawToolParamsObj;
} | {
role: 'user';
content: string;
} | {
role: 'assistant';
content: string;
anthropicReasoning: AnthropicReasoning[] | null;
}
const EMPTY_MESSAGE = '(empty message)'
const CHARS_PER_TOKEN = 4
const TRIM_TO_LEN = 60
// convert messages as if about to send to openai
/*
reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
openai MESSAGE (role=assistant):
"tool_calls":[{
"type": "function",
"id": "call_12345xyz",
"function": {
"name": "get_weather",
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
}]
openai RESPONSE (role=user):
{ "role": "tool",
"tool_call_id": tool_call.id,
"content": str(result) }
also see
openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
*/
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => {
const newMessages: OpenAILLMChatMessage[] = [];
for (let i = 0; i < messages.length; i += 1) {
const currMsg = messages[i]
if (currMsg.role !== 'tool') {
newMessages.push(currMsg)
continue
}
// edit previous assistant message to have called the tool
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
if (prevMsg?.role === 'assistant') {
prevMsg.tool_calls = [{
type: 'function',
id: currMsg.id,
function: {
name: currMsg.name,
arguments: JSON.stringify(currMsg.rawParams)
}
}]
}
// add the tool
newMessages.push({
role: 'tool',
tool_call_id: currMsg.id,
content: currMsg.content,
})
}
return newMessages
}
// convert messages as if about to send to anthropic
/*
https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
anthropic MESSAGE (role=assistant):
"content": [{
"type": "text",
"text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
}, {
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "location": "San Francisco, CA", "unit": "celsius" }
}]
anthropic RESPONSE (role=user):
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "15 degrees"
}]
Converts:
assistant: ...content
tool: (id, name, params)
->
assistant: ...content, call(name, id, params)
user: ...content, result(id, content)
*/
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages;
for (let i = 0; i < messages.length; i += 1) {
const currMsg = messages[i]
// add anthropic reasoning
if (currMsg.role === 'assistant') {
if (currMsg.anthropicReasoning && supportsAnthropicReasoning) {
const content = currMsg.content
newMessages[i] = {
role: 'assistant',
content: content ? [...currMsg.anthropicReasoning, { type: 'text' as const, text: content }] : currMsg.anthropicReasoning
}
}
continue
}
if (currMsg.role === 'user') {
newMessages[i] = {
role: 'user',
content: currMsg.content,
}
continue
}
if (currMsg.role === 'tool') {
// add anthropic tools
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
// make it so the assistant called the tool
if (prevMsg?.role === 'assistant') {
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: currMsg.rawParams })
}
// turn each tool into a user message with tool results at the end
newMessages[i] = {
role: 'user',
content: [{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }]
}
continue
}
}
// we just removed the tools
return newMessages as AnthropicLLMChatMessage[]
}
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const llmChatMessages: LLMChatMessage[] = [];
for (let i = 0; i < messages.length; i += 1) {
const c = messages[i]
const next = 0 <= i + 1 && i + 1 <= messages.length - 1 ? messages[i + 1] : null
if (c.role === 'assistant') {
// if called a tool (message after it), re-add its XML to the message
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content: LLMChatMessage['content'] = c.content
if (next?.role === 'tool') {
content = `${content}\n\n${toolCallXMLStr(next.name, next.rawParams)}`
}
// anthropic reasoning
if (c.anthropicReasoning && supportsAnthropicReasoning) {
content = content ? [...c.anthropicReasoning, { type: 'text' as const, text: content }] : c.anthropicReasoning
}
llmChatMessages.push({
role: 'assistant',
content
})
}
// add user or tool to the previous user message
else if (c.role === 'user' || c.role === 'tool') {
if (c.role === 'tool')
c.content = `<${c.name}_result>\n${c.content}\n</${c.name}_result>`
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
llmChatMessages.push({
role: 'user',
content: c.content
})
else
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
}
}
return llmChatMessages
}
const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const llmChatMessages: LLMChatMessage[] = []
if (!specialToolFormat) { // XML tool behavior
return prepareMessages_XML_tools(messages, supportsAnthropicReasoning)
}
else if (specialToolFormat === 'anthropic-style') {
return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning)
}
else if (specialToolFormat === 'openai-style') {
return prepareMessages_openai_tools(messages)
}
return llmChatMessages
}
// --- CHAT ---
const prepareMessages = ({
messages,
systemMessage,
aiInstructions,
supportsSystemMessage,
specialToolFormat,
supportsAnthropicReasoning,
contextWindow,
maxOutputTokens,
}: {
messages: SimpleLLMMessage[],
systemMessage: string,
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
specialToolFormat: 'openai-style' | 'anthropic-style' | undefined,
supportsAnthropicReasoning: boolean,
contextWindow: number,
maxOutputTokens: number | null | undefined,
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
// ================ trim ================
messages = deepClone(messages)
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
// ================ fit into context ================
// the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES
const alreadyTrimmedIdxes = new Set<number>()
const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => {
const base = message.content.length
let multiplier: number
multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases
if (message.role === 'user') {
multiplier *= 1
}
else {
multiplier *= 10 // llm tokens are far less valuable than user tokens
}
// 1st message, last 3 msgs, any already modified message should be low in weight
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
multiplier *= .05
}
return base * multiplier
}
const _findLargestByWeight = (messages: SimpleLLMMessage[]) => {
let largestIndex = -1
let largestWeight = -Infinity
for (let i = 0; i < messages.length; i += 1) {
const m = messages[i]
const w = weight(m, messages, i)
if (w > largestWeight) {
largestWeight = w
largestIndex = i
}
}
return largestIndex
}
let totalLen = 0
for (const m of messages) { totalLen += m.content.length }
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
// <----------------------------------------->
// 0 | | |
// | contextWindow |
// contextWindow - maxOut|putTokens
// totalLen
let remainingCharsToTrim = charsNeedToTrim
let i = 0
while (remainingCharsToTrim > 0) {
i += 1
if (i > 100) break
const trimIdx = _findLargestByWeight(messages)
const m = messages[trimIdx]
// if can finish here, do
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
if (numCharsWillTrim > remainingCharsToTrim) {
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim()
break
}
remainingCharsToTrim -= numCharsWillTrim
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
alreadyTrimmedIdxes.add(trimIdx)
}
// ================ tools and anthropicReasoning ================
const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning)
// ================ system message concat ================
// find system messages and concatenate them
const newSystemMessage = aiInstructions ?
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
: systemMessage
let separateSystemMessageStr: string | undefined = undefined
// if supports system message
if (supportsSystemMessage) {
if (supportsSystemMessage === 'separated')
separateSystemMessageStr = newSystemMessage
else if (supportsSystemMessage === 'system-role')
llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message
else if (supportsSystemMessage === 'developer-role')
llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message
}
// if does not support system message
else {
const newFirstMessage = {
role: 'user',
content: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
} as const
llmMessages.splice(0, 1) // delete first message
llmMessages.unshift(newFirstMessage) // add new first message
}
// ================ no empty message ================
for (const currMsg of llmMessages) {
if (currMsg.role === 'tool') continue
// if content is a string, replace string with empty msg
if (typeof currMsg.content === 'string')
currMsg.content = currMsg.content || EMPTY_MESSAGE
else {
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
for (const c of currMsg.content) {
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
}
if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
}
}
return {
messages: llmMessages,
separateSystemMessage: separateSystemMessageStr,
} as const
}
export interface IConvertToLLMMessageService {
readonly _serviceBrand: undefined;
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined };
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
prepareFIMMessage(opts: { messages: LLMFIMMessage, aiInstructions: string, }): { prefix: string, suffix: string, stopTokens: string[] }
}
export const IConvertToLLMMessageService = createDecorator<IConvertToLLMMessageService>('ConvertToLLMMessageService');
class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMessageService {
_serviceBrand: undefined;
constructor(
@IModelService private readonly modelService: IModelService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IEditorService private readonly editorService: IEditorService,
@IDirectoryStrService private readonly directoryStrService: IDirectoryStrService,
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
) {
super()
}
// system message
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
const activeURI = this.editorService.activeEditor?.resource?.fsPath;
const directoryStr = await this.directoryStrService.getAllDirectoriesStr({
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ?
`...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const includeXMLToolDefinitions = specialToolFormat === undefined
const runningTerminalIds = this.terminalToolService.listTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions })
return systemMessage
}
// --- LLM Chat messages ---
private _chatMessagesToSimpleMessages(chatMessages: ChatMessage[]): SimpleLLMMessage[] {
const simpleLLMMessages: SimpleLLMMessage[] = []
for (const m of chatMessages) {
if (m.role === 'checkpoint') continue
if (m.role === 'interrupted_streaming_tool') continue
if (m.role === 'assistant') {
simpleLLMMessages.push({
role: m.role,
content: m.displayContent,
anthropicReasoning: m.anthropicReasoning,
})
}
else if (m.role === 'tool') {
simpleLLMMessages.push({
role: m.role,
content: m.content,
name: m.name,
id: m.id,
rawParams: m.rawParams,
})
}
else if (m.role === 'user') {
simpleLLMMessages.push({
role: m.role,
content: m.content,
})
}
}
return simpleLLMMessages
}
prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
const { providerName, modelName } = modelSelection
const {
specialToolFormat,
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
const { messages, separateSystemMessage } = prepareMessages({
messages: simpleMessages,
systemMessage,
aiInstructions,
supportsSystemMessage,
specialToolFormat,
supportsAnthropicReasoning: providerName === 'anthropic',
contextWindow,
maxOutputTokens,
})
return { messages, separateSystemMessage };
}
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
const { providerName, modelName } = modelSelection
const {
specialToolFormat,
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName)
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
const { messages, separateSystemMessage } = prepareMessages({
messages: llmMessages,
systemMessage,
aiInstructions,
supportsSystemMessage,
specialToolFormat,
supportsAnthropicReasoning: providerName === 'anthropic',
contextWindow,
maxOutputTokens,
})
return { messages, separateSystemMessage };
}
// --- FIM ---
prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages, aiInstructions }) => {
let prefix = `\
${!aiInstructions ? '' : `\
// Instructions:
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
${messages.prefix}`
const suffix = messages.suffix
const stopTokens = messages.stopTokens
return { prefix, suffix, stopTokens }
}
}
// pick one and delete the other:
registerSingleton(IConvertToLLMMessageService, ConvertToLLMMessageService, InstantiationType.Eager);
/*
Gemini has this, but they're openai-compat so we don't need to implement this
gemini request:
{ "role": "assistant",
"content": null,
"function_call": {
"name": "get_weather",
"arguments": {
"latitude": 48.8566,
"longitude": 2.3522
}
}
}
gemini response:
{ "role": "assistant",
"function_response": {
"name": "get_weather",
"response": {
"temperature": "15°C",
"condition": "Cloudy"
}
}
}
*/
@@ -14,18 +14,23 @@ import { MAX_CHILDREN_URIs_PAGE } from './toolsService.js';
import { IExplorerService } from '../../files/browser/files.js';
import { SortOrder } from '../../files/common/files.js';
import { ExplorerItem } from '../../files/common/explorerModel.js';
import { VoidDirectoryItem } from '../common/directoryStrTypes.js';
import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
// const MAX_FILES_TOTAL = 200
const MAX_FILES_TOTAL = 300;
const DEFAULT_MAX_DEPTH = 3;
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
const START_MAX_DEPTH = Infinity;
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
export interface IDirectoryStrService {
readonly _serviceBrand: undefined;
getDirectoryStrTool(uri: URI): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
}
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
@@ -53,11 +58,17 @@ const shouldExcludeDirectory = (item: ExplorerItem) => {
item.name === 'obj' ||
item.name === 'vendor' ||
item.name === 'logs' ||
item.name === 'cache'
item.name === 'cache' ||
item.name === 'resource' ||
item.name === 'resources'
) {
return true;
}
if (item.name.match(/\bout\b/)) return true
if (item.name.match(/\bbuild\b/)) return true
return false;
}
@@ -128,130 +139,175 @@ export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], re
// ---------- IN GENERAL ----------
// if the filter exists use it to filter out files and folders when creating the tree
const computeDirectoryTree = async (
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
const computeAndStringifyDirectoryTree = async (
eItem: ExplorerItem,
explorerService: IExplorerService
): Promise<VoidDirectoryItem> => {
// Fetch children with default sort order
const eChildren = await eItem.fetchChildren(SortOrder.FilesFirst);
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem)
// Process children recursively
const children = !isGitIgnoredDirectory ? await Promise.all(
eChildren.map(async c => await computeDirectoryTree(c, explorerService))
) : null
// Create our directory item
const item: VoidDirectoryItem = {
uri: eItem.resource,
name: eItem.name,
isDirectory: eItem.isDirectory,
isSymbolicLink: eItem.isSymbolicLink,
children,
isGitIgnoredDirectory: isGitIgnoredDirectory && { numChildren: eItem.children.size },
};
return item;
};
const stringifyDirectoryTree = (
node: VoidDirectoryItem,
explorerService: IExplorerService,
MAX_CHARS: number,
): { content: string, wasCutOff: boolean } => {
let content = '';
let wasCutOff = false;
fileCount: { count: number } = { count: 0 },
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
): Promise<{ content: string, wasCutOff: boolean }> => {
// Set default values for options
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
const currentDepth = options.currentDepth ?? 0;
const maxItemsPerDir = options.maxItemsPerDir ?? DEFAULT_MAX_ITEMS_PER_DIR;
// Check if we've reached the max depth
if (currentDepth > maxDepth) {
return { content: '', wasCutOff: true };
}
// Check if we've reached the file limit
if (fileCount.count >= MAX_FILES_TOTAL) {
return { content: '', wasCutOff: true };
}
// If we're already exceeding the max characters, return immediately
if (MAX_CHARS <= 0) {
return { content, wasCutOff: true };
return { content: '', wasCutOff: true };
}
// Increment file count
fileCount.count++;
// Add the root node first (without tree characters)
const nodeLine = `${node.name}${node.isDirectory ? '/' : ''}${node.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
const nodeLine = `${eItem.name}${eItem.isDirectory ? '/' : ''}${eItem.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
if (nodeLine.length > MAX_CHARS) {
return { content: '', wasCutOff: true };
}
content += nodeLine;
let content = nodeLine;
let wasCutOff = false;
let remainingChars = MAX_CHARS - nodeLine.length;
// Then recursively add all children with proper tree formatting
if (node.children && node.children.length > 0) {
const { childrenContent, childrenCutOff } = renderChildren(
node.children,
remainingChars,
''
);
content += childrenContent;
wasCutOff = childrenCutOff;
// Check if it's a directory we should skip
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
// Fetch and process children if not a filtered directory
if (eItem.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
// Then recursively add all children with proper tree formatting
if (eChildren && eChildren.length > 0) {
const { childrenContent, childrenCutOff } = await renderChildrenCombined(
eChildren,
remainingChars,
'',
explorerService,
fileCount,
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
);
content += childrenContent;
wasCutOff = childrenCutOff;
}
}
return { content, wasCutOff };
};
// Helper function to render children with proper tree formatting
const renderChildren = (
children: VoidDirectoryItem[],
const renderChildrenCombined = async (
children: ExplorerItem[],
maxChars: number,
parentPrefix: string
): { childrenContent: string, childrenCutOff: boolean } => {
parentPrefix: string,
explorerService: IExplorerService,
fileCount: { count: number },
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
const { maxDepth, currentDepth } = options; // Remove maxItemsPerDir from destructuring
// Get maxItemsPerDir separately and make sure we use it
// For first level (currentDepth = 0), always use Infinity regardless of what was passed
const maxItemsPerDir = currentDepth === 0 ?
Infinity :
(options.maxItemsPerDir ?? DEFAULT_MAX_ITEMS_PER_DIR);
const nextDepth = currentDepth + 1;
let childrenContent = '';
let childrenCutOff = false;
let remainingChars = maxChars;
for (let i = 0; i < children.length; i++) {
const child = children[i];
const isLast = i === children.length - 1;
// Check if we've reached max depth
if (nextDepth > maxDepth) {
return { childrenContent: '', childrenCutOff: true };
}
// Apply maxItemsPerDir limit - only process the specified number of items
const itemsToProcess = maxItemsPerDir === Infinity ? children : children.slice(0, maxItemsPerDir);
const hasMoreItems = children.length > itemsToProcess.length;
for (let i = 0; i < itemsToProcess.length; i++) {
// Check if we've reached the file limit
if (fileCount.count >= MAX_FILES_TOTAL) {
childrenCutOff = true;
break;
}
const child = itemsToProcess[i];
const isLast = (i === itemsToProcess.length - 1) && !hasMoreItems;
// Create the tree branch symbols
const branchSymbol = isLast ? '└── ' : '├── ';
const childLine = `${parentPrefix}${branchSymbol}${child.name}${child.isDirectory ? '/' : ''}${child.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
// Check if adding this line would exceed the limit
if (childrenContent.length + childLine.length > maxChars) {
if (childLine.length > remainingChars) {
childrenCutOff = true;
break;
}
childrenContent += childLine;
remainingChars -= childLine.length;
fileCount.count++;
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
// if gitignored, just say the number of children
if (child.isDirectory && child.isGitIgnoredDirectory && child.isGitIgnoredDirectory.numChildren > 0) {
childrenContent += `${nextLevelPrefix}└── ... (${child.isGitIgnoredDirectory.numChildren} children) ...\n`
}
// Skip processing children for git ignored directories
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
// Create the prefix for the next level (continuation line or space)
else if (child.children && child.children.length > 0) {
if (child.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await child.fetchChildren(SortOrder.Modified);
const {
childrenContent: grandChildrenContent,
childrenCutOff: grandChildrenCutOff
} = renderChildren(
child.children,
maxChars,
nextLevelPrefix
);
if (eChildren && eChildren.length > 0) {
const {
childrenContent: grandChildrenContent,
childrenCutOff: grandChildrenCutOff
} = await renderChildrenCombined(
eChildren,
remainingChars,
nextLevelPrefix,
explorerService,
fileCount,
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
);
// If adding grandchildren content would exceed the limit
if (childrenContent.length + grandChildrenContent.length > maxChars) {
childrenCutOff = true;
break;
}
if (grandChildrenContent.length > 0) {
childrenContent += grandChildrenContent;
remainingChars -= grandChildrenContent.length;
}
childrenContent += grandChildrenContent;
if (grandChildrenCutOff) {
childrenCutOff = true;
break;
if (grandChildrenCutOff) {
childrenCutOff = true;
}
}
}
}
// Add a message if we truncated the items due to maxItemsPerDir
if (hasMoreItems) {
const remainingCount = children.length - itemsToProcess.length;
const truncatedLine = `${parentPrefix}└── (${remainingCount} more items not shown...)\n`;
if (truncatedLine.length <= remainingChars) {
childrenContent += truncatedLine;
remainingChars -= truncatedLine.length;
}
childrenCutOff = true;
}
return { childrenContent, childrenCutOff };
};
@@ -269,12 +325,37 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
super();
}
async getDirectoryStrTool(uri: URI) {
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
const eRoot = this.explorerService.findClosest(uri)
if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`)
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_TOOL);
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
// First try with START_MAX_DEPTH
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_TOOL,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
);
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
let content, wasCutOff;
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_TOOL,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
);
content = result.content;
wasCutOff = result.wasCutOff;
} else {
content = initialContent;
wasCutOff = initialCutOff;
}
let c = content.substring(0, MAX_DIRSTR_CHARS_TOTAL_TOOL)
c = `Directory of ${uri.fsPath}:\n${content}`
@@ -283,13 +364,16 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
return c
}
async getAllDirectoriesStr({ cutOffMessage }: { cutOffMessage: string }) {
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
let str: string = '';
let cutOff = false;
const folders = this.workspaceContextService.getWorkspace().folders;
if (folders.length === 0)
return '(NO WORKSPACE OPEN)';
// Use START_MAX_ITEMS_PER_DIR if not specified
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
for (let i = 0; i < folders.length; i += 1) {
if (i > 0) str += '\n';
@@ -301,9 +385,32 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
const eRoot = this.explorerService.findClosestRoot(rootURI);
if (!eRoot) continue;
// Use our new approach with direct explorer service
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length);
// First try with START_MAX_DEPTH and startMaxItemsPerDir
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
);
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
let content, wasCutOff;
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
);
content = result.content;
wasCutOff = result.wasCutOff;
} else {
content = initialContent;
wasCutOff = initialCutOff;
}
str += content;
if (wasCutOff) {
cutOff = true;
@@ -314,6 +421,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (cutOff) {
return `${str}\n${cutOffMessage}`
}
return str
}
}
@@ -47,6 +47,7 @@ import { IVoidModelService } from '../common/voidModelService.js';
import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@@ -202,6 +203,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IFileService private readonly _fileService: IFileService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
) {
super();
@@ -1233,7 +1235,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
_removeStylesFns: new Set(),
}
console.log('FIRING START STREAMING IN DIFFZONE!!!')
const diffZone = this._addDiffArea(adding)
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
@@ -1267,6 +1268,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
const { from, } = opts
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
@@ -1300,12 +1305,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
const originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n')
const language = model.getLanguageId()
let messages: LLMChatMessage[]
let separateSystemMessage: string | undefined
if (from === 'ClickApply') {
const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language })
messages = [
{ role: 'system', content: rewriteCode_systemMessage, },
{ role: 'user', content: userContent, }
]
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
systemMessage: rewriteCode_systemMessage,
simpleMessages: [{ role: 'user', content: rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }), }],
featureName,
modelSelection,
})
messages = a
separateSystemMessage = b
}
else if (from === 'QuickEdit') {
if (!ctrlKZoneIfQuickEdit) return
@@ -1315,12 +1324,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
const startLine = startRange === 'fullFile' ? 1 : startRange[0]
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine })
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language })
// type: 'messages',
messages = [
{ role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), },
{ role: 'user', content: userContent, }
]
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language })
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
systemMessage: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }),
simpleMessages: [{ role: 'user', content: userContent, }],
featureName,
modelSelection,
})
messages = a
separateSystemMessage = b
}
else { throw new Error(`featureName ${from} is invalid`) }
@@ -1384,10 +1398,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const latestStreamLocationMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
// allowed to throw errors - this is called inside a promise that handles everything
const runWriteover = async () => {
let shouldSendAnotherMessage = true
@@ -1410,6 +1420,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
const { fullText: fullText_ } = params
@@ -1485,6 +1496,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
const { from, applyStr, } = opts
const featureName: FeatureName = 'Apply'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
@@ -1498,10 +1512,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
// build messages - ask LLM to generate search/replace block text
const originalFileCode = model.getValue(EndOfLinePreference.LF)
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
const messages: LLMChatMessage[] = [
{ role: 'system', content: searchReplace_systemMessage },
{ role: 'user', content: userMessageContent },
]
const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
systemMessage: searchReplace_systemMessage,
simpleMessages: [{ role: 'user', content: userMessageContent, }],
featureName,
modelSelection,
})
// if URI is already streaming, return (should never happen, caller is responsible for checking)
if (this._uriIsStreaming(uri)) return
@@ -1593,10 +1610,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
diffZone._streamState.line = 1
const featureName: FeatureName = 'Apply'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const N_RETRIES = 2
// allowed to throw errors - this is called inside a promise that handles everything
@@ -1628,6 +1641,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
const { fullText } = params
@@ -1682,7 +1696,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
console.log('---------')
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
messages.push(
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
{ role: 'assistant', content: fullText }, // latest output
{ role: 'user', content: content } // user explanation of what's wrong
)
@@ -162,7 +162,6 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
)
if (shouldUpdate) {
rerender(c => c + 1)
console.log('rerendering....')
}
}, [applyBoxId, uri]))
@@ -322,12 +321,12 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
<IconShell1
Icon={X}
onClick={onReject}
{...tooltipPropsForApplyBlock({ tooltipName: 'Reject file' })}
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
/>
<IconShell1
Icon={Check}
onClick={onAccept}
{...tooltipPropsForApplyBlock({ tooltipName: 'Accept file' })}
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
/>
</>
}
@@ -57,7 +57,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
let link = undefined
if (rawText.endsWith("`")) { // if codespan was completed
if (rawText.endsWith('`')) { // if codespan was completed
// get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
@@ -120,11 +120,11 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return null;
}
if (t.type === "space") {
if (t.type === 'space') {
return <span>{t.raw}</span>
}
if (t.type === "code") {
if (t.type === 'code') {
const [firstLine, remainingContents] = separateOutFirstLine(t.text)
const firstLineIsURI = isValidUri(firstLine) && !codeURI
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
@@ -152,7 +152,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
}
if (options.isApplyEnabled && chatMessageLocation) {
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with "```")
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with '```')
const applyBoxId = getApplyBoxId({
threadId: chatMessageLocation.threadId,
@@ -179,7 +179,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
/>
}
if (t.type === "heading") {
if (t.type === 'heading') {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
@@ -188,7 +188,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
</HeadingTag>
}
if (t.type === "table") {
if (t.type === 'table') {
return (
<div>
<table>
@@ -217,14 +217,14 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
)
// return (
// <div>
// <table className={"min-w-full border border-void-bg-2"}>
// <table className={'min-w-full border border-void-bg-2'}>
// <thead>
// <tr className="bg-void-bg-1">
// <tr className='bg-void-bg-1'>
// {t.header.map((cell: any, index: number) => (
// <th
// key={index}
// className="px-4 py-2 border border-void-bg-2 font-semibold"
// style={{ textAlign: t.align[index] || "left" }}
// className='px-4 py-2 border border-void-bg-2 font-semibold'
// style={{ textAlign: t.align[index] || 'left' }}
// >
// {cell.raw}
// </th>
@@ -237,8 +237,8 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
// {row.map((cell: any, cellIndex: number) => (
// <td
// key={cellIndex}
// className={"px-4 py-2 border border-void-bg-2"}
// style={{ textAlign: t.align[cellIndex] || "left" }}
// className={'px-4 py-2 border border-void-bg-2'}
// style={{ textAlign: t.align[cellIndex] || 'left' }}
// >
// {cell.raw}
// </td>
@@ -251,32 +251,32 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
// )
}
if (t.type === "hr") {
if (t.type === 'hr') {
return <hr />
}
if (t.type === "blockquote") {
if (t.type === 'blockquote') {
return <blockquote>{t.text}</blockquote>
}
if (t.type === 'list_item') {
return <li>
<input type="checkbox" checked={t.checked} readOnly />
<input type='checkbox' checked={t.checked} readOnly />
<span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} />
</span>
</li>
}
if (t.type === "list") {
const ListTag = t.ordered ? "ol" : "ul"
if (t.type === 'list') {
const ListTag = t.ordered ? 'ol' : 'ul'
return (
<ListTag start={t.start ? t.start : undefined}>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
<input type='checkbox' checked={item.checked} readOnly />
)}
<span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} inPTag={true} {...options} />
@@ -287,7 +287,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
)
}
if (t.type === "paragraph") {
if (t.type === 'paragraph') {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index}
@@ -304,34 +304,28 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <p>{contents}</p>
}
if (t.type === "html") {
const contents = t.raw
if (inPTag) return <span className='block'>{contents}</span>
return <p>{contents}</p>
}
if (t.type === "text" || t.type === "escape") {
if (t.type === 'text' || t.type === 'escape' || t.type === 'html') {
return <span>{t.raw}</span>
}
if (t.type === "def") {
if (t.type === 'def') {
return <></> // Definitions are typically not rendered
}
if (t.type === "link") {
if (t.type === 'link') {
return (
<a
onClick={() => { window.open(t.href) }}
href={t.href}
title={t.title ?? undefined}
className='underline cursor-pointer hover:brightness-90 transition-all duration-200'
className='underline cursor-pointer hover:brightness-90 transition-all duration-200 text-void-fg-2'
>
{t.text}
</a>
)
}
if (t.type === "image") {
if (t.type === 'image') {
return <img
src={t.href}
alt={t.text}
@@ -340,16 +334,16 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
/>
}
if (t.type === "strong") {
if (t.type === 'strong') {
return <strong>{t.text}</strong>
}
if (t.type === "em") {
if (t.type === 'em') {
return <em>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
if (t.type === 'codespan') {
if (options.isLinkDetectionEnabled && chatMessageLocation) {
return <CodespanWithLink
@@ -363,18 +357,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <Codespan text={t.text} />
}
if (t.type === "br") {
if (t.type === 'br') {
return <br />
}
// strikethrough
if (t.type === "del") {
if (t.type === 'del') {
return <del>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
<span className="text-sm text-orange-500">Unknown token rendered...</span>
<div className='bg-orange-50 rounded-sm overflow-hidden p-2'>
<span className='text-sm text-orange-500'>Unknown token rendered...</span>
</div>
)
}
@@ -20,14 +20,15 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X } from 'lucide-react';
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag } from 'lucide-react';
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
import { ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { LintErrorItem, ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
import { IsRunningType } from '../../../chatThreadService.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { PlacesType } from 'react-tooltip';
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
@@ -660,7 +661,8 @@ type ToolHeaderParams = {
children?: React.ReactNode;
bottomChildren?: React.ReactNode;
onClick?: () => void;
isOpen?: boolean,
isOpen?: boolean;
className?: string;
}
const ToolHeaderWrapper = ({
@@ -671,10 +673,12 @@ const ToolHeaderWrapper = ({
numResults,
hasNextPage,
children,
bottomChildren,
isError,
onClick,
isOpen,
isRejected,
className, // applies to the main content
}: ToolHeaderParams) => {
const [isOpen_, setIsOpen] = useState(false);
@@ -684,7 +688,7 @@ const ToolHeaderWrapper = ({
const isClickable = !!(isDropdown || onClick)
return (<div className=''>
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ">
<div className={`w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ${className}`}>
{/* header */}
<div className={`select-none flex items-center min-h-[24px] ${!isDropdown ? 'mx-1' : ''}`}>
<div className={`flex items-center w-full gap-x-2 overflow-hidden justify-between ${isRejected ? 'line-through' : ''}`}>
@@ -733,6 +737,7 @@ const ToolHeaderWrapper = ({
{children}
</div>}
</div>
{bottomChildren}
</div>);
};
@@ -1167,11 +1172,12 @@ const titleOfToolName = {
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
'get_dir_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') },
'search_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') },
'search_for_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') },
'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) },
'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) },
'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') },
'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }
'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') },
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type'>): React.ReactNode => {
@@ -1200,8 +1206,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
} else if (toolName === 'search_pathnames_only') {
const toolParams = _toolParams as ToolCallParams['search_pathnames_only']
return `"${toolParams.queryStr}"`;
} else if (toolName === 'search_files') {
const toolParams = _toolParams as ToolCallParams['search_files']
} else if (toolName === 'search_for_files') {
const toolParams = _toolParams as ToolCallParams['search_for_files']
return `"${toolParams.queryStr}"`;
} else if (toolName === 'create_file_or_folder') {
const toolParams = _toolParams as ToolCallParams['create_file_or_folder']
@@ -1292,7 +1298,7 @@ const ToolRequestAcceptRejectButtons = () => {
</div>
)
return <div className="flex gap-2 my-1 items-center">
return <div className="flex gap-2 mx-4 items-center">
{approveButton}
{cancelButton}
{autoApproveToggle}
@@ -1330,7 +1336,7 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => {
const EditToolChildren = ({ uri, changeDescription }: { uri: URI | undefined, changeDescription: string }) => {
return <div className='!select-text cursor-auto'>
<SmallProseWrapper>
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
@@ -1338,6 +1344,44 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript
</div>
}
const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
return <div className="text-xs text-void-fg-4 opacity-80 border-l-2 border-void-warning px-2 py-0.5 flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap">
{lintErrors.map((error, i) => (
<div key={i}>Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
</div>
}
const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
if (lintErrors.length === 0) return null;
const [isOpen, setIsOpen] = useState(false);
return (
<div className="w-full px-2 mt-0.5">
<div
className={`flex items-center cursor-pointer select-none transition-colors duration-150 pl-0 py-0.5 rounded group`}
onClick={() => setIsOpen(o => !o)}
style={{ background: 'none' }}
>
<ChevronRight
className={`mr-1 h-4 w-4 flex-shrink-0 transition-transform duration-100 text-void-fg-4 group-hover:text-void-fg-3 ${isOpen ? 'rotate-90' : ''}`}
/>
<span className="font-medium text-void-fg-4 group-hover:text-void-fg-3 text-xs">Lint errors</span>
</div>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${isOpen ? 'opacity-100 py-1' : 'max-h-0 opacity-0'} text-xs pl-5`}
>
<div className="flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap text-void-fg-4 opacity-90 border-l-2 border-void-warning px-2 py-0.5">
{lintErrors.map((error, i) => (
<div key={i} className="">Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
</div>
</div>
</div>
);
}
const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: string, uri: URI, codeStr: string }) => {
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
return <div className='flex items-center gap-1'>
@@ -1399,11 +1443,18 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const isError = toolMessage.type === 'tool_error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) {
const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}`
const end = toolMessage.params.endLine === null ? `` : `${toolMessage.params.endLine}`
const addStr = `(${start}-${end})`
componentParams.desc1 += ` ${addStr}`
}
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
if (result.hasNextPage && params.pageNumber === 1) // first page
componentParams.desc2 = '(more content available)'
componentParams.desc2 = `(truncated after ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)`
else if (params.pageNumber > 1) // subsequent pages
componentParams.desc2 = `(part ${params.pageNumber})`
}
@@ -1554,7 +1605,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <ToolHeaderWrapper {...componentParams} />
}
},
'search_files': {
'search_for_files': {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
@@ -1598,6 +1649,47 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
}
},
'read_lint_errors': {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = getTitle(toolMessage)
const { uri } = toolMessage.params ?? {}
const desc1 = uri ? getBasename(uri.fsPath) : '';
const icon = null
if (toolMessage.type === 'tool_request') return null
if (toolMessage.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'running_now') return null // do not show running
const isError = toolMessage.type === 'tool_error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
if (result.lintErrors)
componentParams.children = <LintErrorChildren lintErrors={result.lintErrors} />
else
componentParams.children = `No lint errors found.`
}
else if (toolMessage.type === 'tool_error') {
const { params, result } = toolMessage
if (params) componentParams.desc2 = <JumpToFileButton uri={params.uri} />
componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
return <ToolHeaderWrapper {...componentParams} />
},
},
// ---
'create_file_or_folder': {
@@ -1726,7 +1818,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
if (toolMessage.type !== 'tool_error') {
const { params, result } = toolMessage
// componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
@@ -1842,28 +1934,30 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIsRunning }: { message: CheckpointEntry, threadId: string; messageIdx: number, isCheckpointGhost: boolean, threadIsRunning: boolean }) => {
const accessor = useAccessor()
const chatThreadService = accessor.get('IChatThreadService')
const [showCheckpointIcon, setShowCheckpointIcon] = React.useState(false); // add icon state
return <div
className={`flex items-center justify-center px-2 `}
>
<div
className={`
text-xs
text-void-fg-3
cursor-pointer select-none
${isCheckpointGhost ? 'opacity-50' : 'opacity-100'}
`}
text-xs
text-void-fg-3
cursor-pointer select-none
${isCheckpointGhost ? 'opacity-50' : 'opacity-100'}
`}
style={{ position: 'relative', display: 'inline-block' }} // allow absolute icon
onClick={() => {
if (threadIsRunning) return
chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
}}
>
Checkpoint
Checkpoint
</div>
</div>
}
type ChatBubbleMode = 'display' | 'edit'
type ChatBubbleProps = {
chatMessage: ChatMessage,
@@ -2260,6 +2354,37 @@ const CommandBarInChat = () => {
}
const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => {
const uri = URI.file(toolCallSoFar.rawParams.uri ?? 'unknown')
const title = titleOfToolName['edit_file'].proposed
const uriDone = toolCallSoFar.doneParams.includes('uri')
const desc1 = <span className='flex items-center'>
{uriDone ?
getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown')
: `Generating`}
<IconLoading />
</span>
// If URI has not been specified
return <ToolHeaderWrapper
title={title}
desc1={desc1}
desc2={<JumpToFileButton uri={uri} />}
>
<EditToolChildren
uri={uri}
changeDescription={toolCallSoFar.rawParams.change_description ?? ''}
/>
<IconLoading />
</ToolHeaderWrapper>
}
export const SidebarChat = () => {
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
@@ -2379,7 +2504,6 @@ export const SidebarChat = () => {
role: 'assistant',
displayContent: displayContentSoFar ?? '',
reasoning: reasoningSoFar ?? '',
toolCall: toolCallSoFar,
anthropicReasoning: null,
}}
messageIdx={streamingChatIdx}
@@ -2391,6 +2515,15 @@ export const SidebarChat = () => {
/> : null
// the tool currently being generated
const generatingTool = toolIsGenerating ?
toolCallSoFar.name === 'edit_file' ? <EditToolSoFar
key={getChatBubbleId(threadId, streamingChatIdx + 1)}
toolCallSoFar={toolCallSoFar}
/>
: null
: null
const messagesHTML = <ScrollToBottomContainer
key={'messages' + chatThreadsState.currentThreadId} // force rerender on all children if id changes
scrollContainerRef={scrollContainerRef}
@@ -2407,17 +2540,11 @@ export const SidebarChat = () => {
{previousMessagesHTML}
{currStreamingMessageHTML}
{toolIsGenerating ?
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)}
title={toolCallSoFar && toolNames.includes(toolCallSoFar.name as ToolName) ?
titleOfToolName[toolCallSoFar.name as ToolName]?.proposed
: toolCallSoFar?.name}
desc1={<span className='flex items-center'>Generating<IconLoading /></span>}
/>
: null}
{/* Generating tool */}
{generatingTool}
{/* loading indicator */}
{isRunning === 'LLM' && !toolIsGenerating ? <ProseWrapper>
{/* loading indicator */}
{<IconLoading className='opacity-50 text-sm' />}
</ProseWrapper> : null}
@@ -418,6 +418,7 @@ export const VoidSwitch = ({
onChange,
size = 'md',
disabled = false,
...props
}: {
value: boolean;
onChange: (value: boolean) => void;
@@ -425,7 +426,7 @@ export const VoidSwitch = ({
size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md';
}) => {
return (
<label className="inline-flex items-center">
<label className="inline-flex items-center" {...props}>
<div
onClick={() => !disabled && onChange(!value)}
className={`
@@ -47,6 +47,7 @@ import { IWorkspaceContextService } from '../../../../../../../platform/workspac
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
import { INativeHostService } from '../../../../../../../platform/native/common/native.js';
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
import { IToolsService } from '../../../toolsService.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@@ -215,6 +216,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
INativeHostService: accessor.get(INativeHostService),
IToolsService: accessor.get(IToolsService),
} as const
return reactAccessor
@@ -245,12 +245,12 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
// </button>
const acceptAllButton = <AcceptAllButtonWrapper
text={'Accept File'}
text={'Keep Changes'}
onClick={onAcceptAll}
/>
const rejectAllButton = <RejectAllButtonWrapper
text={'Reject File'}
text={'Reject All'}
onClick={onRejectAll}
/>
@@ -0,0 +1,747 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useEffect, useState } from 'react';
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
import { Check, ExternalLink, X } from 'lucide-react';
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
const OVERRIDE_VALUE = true
export const VoidOnboarding = () => {
const voidSettingsState = useSettingsState()
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete || OVERRIDE_VALUE
const isDark = useIsDark()
return (
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div
className={`
hidden
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
>
<VoidOnboardingContent />
</div>
</div>
)
}
const FADE_DURATION_MS = 2000
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
const [opacity, setOpacity] = useState(0)
useEffect(() => {
const timeout = setTimeout(() => {
setOpacity(1)
}, delayMs)
return () => clearTimeout(timeout)
}, [setOpacity, delayMs])
return (
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
{children}
</div>
)
}
// Onboarding
// OnboardingPage
// title:
// div
// "Welcome to Void"
// image
// content:<></>
// title
// content
// prev/next
// OnboardingPage
// title:
// div
// "How would you like to use Void?"
// content:
// ModelQuestionContent
// |
// div
// "I want to:"
// div
// "Use the smartest models"
// "Keep my data fully private"
// "Save money"
// "I don't know"
// | div
// | div
// "We recommend using "
// "Set API"
// | div
// ""
// | div
//
// title
// content
// prev/next
//
// OnboardingPage
// title
// content
// prev/next
const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 bg-[#0e70c0] enabled:hover:bg-[#1177cb] disabled:opacity-50 disabled:cursor-not-allowed rounded text-white duration-300 transition-all"
{...props.disabled && {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': 'Disabled (Please enter all required fields or choose another Provider)',
'data-tooltip-place': 'top',
}}
{...props}
>
Next
</button>
)
}
const SkipButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3 text-void-fg-2 duration-300 transition-all"
{...props}
>
Skip
</button>
)
}
const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded text-void-fg-3 opacity-80 hover:brightness-110 duration-300 transition-all"
{...props}
>
Back
</button>
)
}
const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, className = '', }: {
top?: React.ReactNode,
bottom?: React.ReactNode,
content?: React.ReactNode,
hasMaxWidth?: boolean,
className?: string,
}) => {
return (
<div className={`min-h-full flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
<FadeIn className='w-full pt-16'>{top}</FadeIn>
<FadeIn className='w-full my-auto'>{content}</FadeIn>
<div className='w-full pb-8'>{bottom}</div>
</div>
)
}
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
// for now just link to the ollama download page
return <a
href={`https://ollama.com/library/${modelName}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
// if (isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Uninstall</span>
// <IconShell1
// className="ml-1"
// Icon={Trash}
// onClick={() => {
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (isModelInstalling) {
// return <div className="flex items-center">
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
// <IconShell1
// className="ml-1"
// Icon={Square}
// onClick={() => {
// // abort()
// // TODO!!!!!!!!!!! don't do this
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (!isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
// <IconShell1
// className="ml-1"
// Icon={Download}
// onClick={() => {
// // this is a check for whether the model was installed:
// if (isModelInstalling) return
// // TODO!!!!!! don't do this
// // install(modelname), callback = setIsModelInstalling(false);
// setIsModelInstalling(true);
// }}
// />
// </div>
// }
// return <></>
}
const YesNoText = ({ val }: { val: boolean | null }) => {
return <div
className={
val === true ? "text text-green-500"
: val === false ? 'text-red-500'
: "text text-yellow-500"
}
>
{
val === true ? "Yes"
: val === false ? 'No'
: "Yes*"
}
</div>
}
const abbreviateNumber = (num: number): string => {
if (num >= 1000000) {
// For millions
return Math.floor(num / 1000000) + 'M';
} else if (num >= 1000) {
// For thousands
return Math.floor(num / 1000) + 'K';
} else {
// For numbers less than 1000
return num.toString();
}
}
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
// const providerCapabilities = getProviderCapabilities(providerName)
// info used to show the table
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.isDefault,
isDownloaded: true
}
})
// special case columns for ollama; show recommended models as default
if (providerName === 'ollama') {
for (const modelName of ollamaRecommendedModels) {
if (modelName in infoOfModelName) continue
infoOfModelName[modelName] = {
...infoOfModelName[modelName],
showAsDefault: true,
}
}
}
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
<thead>
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
</tr>
</thead>
<tbody>
{Object.keys(infoOfModelName).map(modelName => {
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
const capabilities = getModelCapabilities(providerName, modelName)
const {
downloadable,
cost,
supportsFIM,
reasoningCapabilities,
contextWindow,
isUnrecognizedModel,
maxOutputTokens,
supportsSystemMessage,
} = capabilities
// TODO update this when tools work
const supportsTools = !!!((capabilities as unknown as any).supportsTools)
const removeModelButton = <button
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
>
<X className="w-3.5 h-3.5" />
</button>
return (
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<td className="py-2 px-3 relative">
{!showAsDefault && removeModelButton}
{modelName}
</td>
<td className="py-2 px-3">${cost.output ?? ''}</td>
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
<td className="py-2 px-3"><YesNoText val={true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsTools} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
)
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true} />
</td>
<td colSpan={4}></td>
</tr>
</tbody>
</table>
}
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
const VoidOnboardingContent = () => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const [pageIndex, setPageIndex] = useState(0)
// page 1 state
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
// page 2 state
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(null)
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
private: ['ollama', 'vLLM', 'openAICompatible'],
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
all: providerNames,
// TODO allow user to redo onboarding
}
const didFillInProviderSettings = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName]._didFillInProviderSettings
const isApiKeyLongEnoughIfApiKeyExists = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].apiKey ? voidSettingsState.settingsOfProvider[selectedProviderName].apiKey.length > 15 : true
const isAtLeastOneModel = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].models.length >= 1
const didFillInSelectedProviderSettings = !!(didFillInProviderSettings && isApiKeyLongEnoughIfApiKeyExists && isAtLeastOneModel)
const prevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
<div className="flex items-center gap-4">
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
<NextButton
onClick={() => { setPageIndex(pageIndex + 1) }}
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
/>
</div>
</div>
// cannot be md
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Models with the best performance on benchmarks.",
private: "Fully private and hosted on your computer/network.",
cheap: "Free and low-cost options.",
all: "",
}
// can be md
const detailedDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Most intelligent and best for agent mode.",
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:founders@voideditor.com) for help setting up at your company.",
cheap: "Great deals like Gemini 2.5 Pro or self-host a model with Ollama or vLLM for free.",
all: "",
}
// set the selected provider name appropriately
useEffect(() => {
if (wantToUseOption && providerNamesOfWantToUseOption[wantToUseOption].length > 0) {
setSelectedProviderName(providerNamesOfWantToUseOption[wantToUseOption][0]);
} else {
setSelectedProviderName(null);
}
}, [wantToUseOption]);
// set wantToUseOption to smart when page changes
useEffect(() => {
setWantToUseOption(wantToUseOption);
}, [pageIndex]);
// reset the page to page 0 if the user redos onboarding
useEffect(() => {
if (!voidSettingsState.globalSettings.isOnboardingComplete) {
setPageIndex(0)
}
}, [setPageIndex, voidSettingsState.globalSettings.isOnboardingComplete])
// TODO add a description next to the skip button saying (you can always restart the onboarding in Settings)
const contentOfIdx: { [pageIndex: number]: React.ReactNode } = {
// 0: <OnboardingPageShell
// top={
// <div className='bg-green-600 h-6 w-32' />
// }
// content={
// <div className='bg-red-600 h-[10000px] w-32' />
// }
// bottom={
// <div className='bg-blue-600 h-6 w-32' />
// }
// />,
0: <OnboardingPageShell
top={
<div className="text-5xl font-light text-center">Welcome to Void</div>
}
content={
<FadeIn
delayMs={500}
className="text-center"
onClick={() => { setPageIndex(pageIndex + 1) }}
>
Get Started
</FadeIn>
}
bottom={
''
}
/>,
1: <OnboardingPageShell
hasMaxWidth={false}
top={
<FadeIn className='flex flex-col items-center'>
<div className="text-5xl font-light text-center">AI Preferences</div>
<div className="mt-[10%] text-base text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
<div className="flex justify-center w-full md:flex-nowrap md:max-w-[80%] max-w-[90%] gap-4">
<div
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">🧠</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Intelligence</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['smart']}</p>
</div>
<div
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">🔒</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Privacy</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['private']}</p>
</div>
<div
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">💵</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Affordability</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['cheap']}</p>
</div>
</div>
</FadeIn>
}
content={<></>}
/>,
2: <OnboardingPageShell
top={
<div className='flex flex-col items-center'>
{/* Title */}
<div className="text-5xl font-light text-center">Choose a Provider</div>
{/* Preference Selector */}
<div className="mt-6 mb-6 mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md">
<button
onClick={() => {
setWantToUseOption('smart');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'smart'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Intelligent
</button>
<button
onClick={() => {
setWantToUseOption('private');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'private'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Private
</button>
<button
onClick={() => {
setWantToUseOption('cheap');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'cheap'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Low-Cost
</button>
<button
onClick={() => {
setWantToUseOption('all')
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'all'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
All
</button>
</div>
{/* Provider Buttons */}
<div
key={wantToUseOption}
className="mb-2 flex flex-wrap items-center w-full"
>
{(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => {
const isSelected = selectedProviderName === providerName
return (
<button
key={providerName}
onClick={() => setSelectedProviderName(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-colors duration-150 border
${isSelected ? 'bg-[#0e70c0] text-white shadow-sm border-[#0e70c0]/80' : 'bg-[#0e70c0]/10 text-void-fg-3 hover:bg-[#0e70c0]/30 border-[#0e70c0]/20'}
`}
>
{displayInfoOfProviderName(providerName).title}
</button>
)
})}
</div>
{/* Description */}
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4'>
{/* Models Table */}
<TableOfModelsForProvider providerName={selectedProviderName} />
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8'>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
</div>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
{/* Button and status indicators */}
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <div className="mt-2"><AnimatedCheckmarkButton text='Added' /></div>}
</div>
</div>}
</div>
}
bottom={
prevAndNextButtons
}
/>,
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
// <FadeIn>
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
// </div>
// </FadeIn>
// {prevAndNextButtons}
// </div>,
3: <OnboardingPageShell
top={
<div>
<div className="text-5xl font-light text-center">Settings and Themes</div>
<div className="mt-8 text-center flex flex-col items-center gap-4 w-full max-w-md mx-auto">
<h4 className="text-void-fg-3 mb-4">Transfer your settings from an existing editor?</h4>
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="VS Code" />
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Cursor" />
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Windsurf" />
</div>
</div>
}
bottom={prevAndNextButtons}
/>,
4: <OnboardingPageShell
top={
<div className="text-5xl font-light text-center">Jump in</div>
}
content={
<div
className="text-center"
onClick={() => {
// TODO make a fadeout effect
voidSettingsService.setGlobalSetting('isOnboardingComplete', true)
}}
>
Enter the Void
</div>
}
bottom={
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
}
/>,
}
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-auto flex flex-col items-center justify-around">
{contentOfIdx[pageIndex]}
</div>
}
@@ -0,0 +1,9 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { VoidOnboarding } from './VoidOnboarding.js'
export const mountVoidOnboarding = mountFnGenerator(VoidOnboarding)
@@ -3,23 +3,19 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButtonBgDarken, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check, MoveRight, PlusCircle, MinusCircle, Download, Trash, StopCircle, Square, ExternalLink } from 'lucide-react'
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
import { X, RefreshCw, Loader2, Check, } from 'lucide-react'
import { URI } from '../../../../../../../base/common/uri.js'
import { env } from '../../../../../../../base/common/process.js'
import { ModelDropdown } from './ModelDropdown.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
import { WarningBox } from './WarningBox.js'
import { os } from '../../../../common/helpers/systemInfo.js'
import { IconLoading, IconX } from '../sidebar-tsx/SidebarChat.js'
import { getModelCapabilities, getProviderCapabilities, ollamaRecommendedModels, VoidStaticModelInfo } from '../../../../common/modelCapabilities.js'
import VoidImage from './VoidImage.js'
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
@@ -100,7 +96,7 @@ const RefreshableModels = () => {
const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => {
export const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => {
const [dashOffset, setDashOffset] = useState(40);
useEffect(() => {
@@ -158,7 +154,7 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t
// shows a providerName dropdown if no `providerName` is given
const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
const accessor = useAccessor()
const settingsStateService = accessor.get('IVoidSettingsService')
@@ -166,19 +162,22 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
const settingsState = useSettingsState()
const [isOpen, setIsOpen] = useState(false)
const [showCheckmark, setShowCheckmark] = useState(false)
// const providerNameRef = useRef<ProviderName | null>(null)
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null)
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
const providerName = permanentProviderName ?? userChosenProviderName;
const [modelName, setModelName] = useState<string>('')
const [errorString, setErrorString] = useState('')
if (!providerName) { return null; }
const numModels = settingsState.settingsOfProvider[providerName].models.length
if (showCheckmark) {
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
}
if (!isOpen) {
return <div
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
@@ -207,9 +206,8 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionsEqual={(a, b) => a === b}
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
py-[4px] px-[6px]
`}
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
arrowTouchesText={false}
/>
}
@@ -220,7 +218,7 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
onChangeValue={setModelName}
placeholder='Model Name'
compact={compact}
className={'max-w-44'}
className={'max-w-32'}
/>
{/* add button */}
@@ -244,7 +242,11 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
}
settingsStateService.addModel(providerName, modelName)
setIsOpen(false)
setShowCheckmark(true)
setTimeout(() => {
setShowCheckmark(false)
setIsOpen(false)
}, 1500)
setErrorString('')
setModelName('')
}}
@@ -288,7 +290,16 @@ export const ModelDump = () => {
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
const providerTitle = displayInfoOfProviderName(providerName).title
const disabled = !providerEnabled
const value = disabled ? false : !isHidden
const tooltipName = (
disabled ? `Add ${providerTitle} to enable`
: value === true ? 'Enabled'
: 'Disabled'
)
return <div key={`${modelName}${providerName}`}
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
@@ -296,18 +307,28 @@ export const ModelDump = () => {
>
{/* left part is width:full */}
<div className={`flex-grow flex items-center gap-4`}>
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
<span className='w-fit truncate'>{modelName}</span>
</div>
{/* right part is anything that fits */}
<div className='flex items-center gap-4'>
<div className='flex items-center gap-4'
// data-tooltip-id='void-tooltip'
// data-tooltip-place='top'
// data-tooltip-content={disabled ? `${displayInfoOfProviderName(providerName).title} is disabled`
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
// }
>
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<VoidSwitch
value={disabled ? false : !isHidden}
value={value}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
disabled={disabled}
size='sm'
data-tooltip-id='void-tooltip'
data-tooltip-place='right'
data-tooltip-content={tooltipName}
/>
<div className={`w-5 flex items-center justify-center`}>
@@ -402,7 +423,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
// </div >
// }
const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
const voidSettingsState = useSettingsState()
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
@@ -524,6 +545,29 @@ const FastApplyMethodDropdown = () => {
}
export const ollamaSetupInstructions = <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
const RedoOnboardingButton = ({ className }: { className?: string }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
return <div
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', false) }}
>
See onboarding screen?
</div>
}
export const FeaturesTab = () => {
const voidSettingsState = useSettingsState()
const accessor = useAccessor()
@@ -534,7 +578,8 @@ export const FeaturesTab = () => {
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
<ModelDump />
<AddModelInputBox />
<AddModelInputBox className='mt-4' compact />
<RedoOnboardingButton className='mt-2 mb-4' />
<AutoDetectLocalModelsToggle />
<RefreshableModels />
</ErrorBoundary>
@@ -570,7 +615,19 @@ export const FeaturesTab = () => {
{/* FIM */}
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Experimental. Only works with models that support FIM.</div>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>
<span>
Experimental. Only works with FIM models.
</span>
<span
className='hover:brightness-110'
data-tooltip-id='void-tooltip'
data-tooltip-content='We recommend using qwen2.5-coder:1.5b with Ollama.'
data-tooltip-class-name='void-max-w-[20px]'
>
*
</span>
</div>
<div className='my-2'>
{/* Enable Switch */}
@@ -642,6 +699,16 @@ export const FeaturesTab = () => {
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}</span>
</div>
{/* Tool Lint Errors Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.includeToolLintErrors}
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Don't fix lint errors`}</span>
</div>
</div>
</div>
@@ -809,7 +876,7 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
}
const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
const accessor = useAccessor()
const fileService = accessor.get('IFileService')
@@ -891,7 +958,7 @@ const GeneralTab = () => {
return <>
<div className=''>
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
<h4 className={`text-void-fg-3 mb-2`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
<div className='flex flex-col gap-4'>
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
@@ -904,7 +971,7 @@ const GeneralTab = () => {
<div className='mt-12'>
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
<h4 className={`text-void-fg-3 mb-2`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
@@ -931,7 +998,7 @@ const GeneralTab = () => {
<div className='mt-12 max-w-[600px]'>
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
<h4 className={`text-void-fg-3 mb-2`}>{`Instructions to include on all AI requests.`}</h4>
<h4 className={`text-void-fg-3 mb-4`}>{`Instructions to include on all AI requests.`}</h4>
<AIInstructionsBox />
</div>
@@ -946,14 +1013,6 @@ export const Settings = () => {
const [tab, setTab] = useState<TabName>('models')
const deleteme = false
if (deleteme) {
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
<VoidOnboarding />
</div>
}
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
@@ -1000,663 +1059,3 @@ export const Settings = () => {
</div>
}
const FADE_DURATION_MS = 2000
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
const [opacity, setOpacity] = useState(0)
useEffect(() => {
const timeout = setTimeout(() => {
setOpacity(1)
}, delayMs)
return () => clearTimeout(timeout)
}, [setOpacity, delayMs])
return (
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
{children}
</div>
)
}
// Onboarding
// OnboardingPage
// title:
// div
// "Welcome to Void"
// image
// content:<></>
// title
// content
// prev/next
// OnboardingPage
// title:
// div
// "How would you like to use Void?"
// content:
// ModelQuestionContent
// |
// div
// "I want to:"
// div
// "Use the smartest models"
// "Keep my data fully private"
// "Save money"
// "I don't know"
// | div
// | div
// "We recommend using "
// "Set API"
// | div
// ""
// | div
//
// title
// content
// prev/next
//
// OnboardingPage
// title
// content
// prev/next
const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-accent hover:bg-void-accent/90 text-white"
{...props}
>
Next
</button>
)
}
const SkipButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3 text-void-fg-2"
{...props}
>
Skip
</button>
)
}
const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3"
{...props}
>
Previous
</button>
)
}
const ollamaSetupInstructions = <div className='prose-p:my-0 prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm font-light list-decimal select-text opacity-80'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
// for now just link to the ollama download page
return <a
href={`https://ollama.com/library/${modelName}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
// if (isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Uninstall</span>
// <IconShell1
// className="ml-1"
// Icon={Trash}
// onClick={() => {
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (isModelInstalling) {
// return <div className="flex items-center">
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
// <IconShell1
// className="ml-1"
// Icon={Square}
// onClick={() => {
// // abort()
// // TODO!!!!!!!!!!! don't do this
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (!isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
// <IconShell1
// className="ml-1"
// Icon={Download}
// onClick={() => {
// // this is a check for whether the model was installed:
// if (isModelInstalling) return
// // TODO!!!!!! don't do this
// // install(modelname), callback = setIsModelInstalling(false);
// setIsModelInstalling(true);
// }}
// />
// </div>
// }
// return <></>
}
const YesNoText = ({ val }: { val: boolean | null }) => {
return <div
className={
val === true ? "text text-green-500"
: val === false ? 'text-red-500'
: "text text-yellow-500"
}
>
{
val === true ? "Yes"
: val === false ? 'No'
: "Yes*"
}
</div>
}
const abbreviateNumber = (num: number): string => {
if (num >= 1000000) {
// For millions
return Math.floor(num / 1000000) + 'M';
} else if (num >= 1000) {
// For thousands
return Math.floor(num / 1000) + 'K';
} else {
// For numbers less than 1000
return num.toString();
}
}
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
// const providerCapabilities = getProviderCapabilities(providerName)
// info used to show the table
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.isDefault,
isDownloaded: true
}
})
// special case columns for ollama; show recommended models as default
if (providerName === 'ollama') {
for (const modelName of ollamaRecommendedModels) {
if (modelName in infoOfModelName) continue
infoOfModelName[modelName] = {
...infoOfModelName[modelName],
showAsDefault: true,
}
}
}
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
<thead>
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
</tr>
</thead>
<tbody>
{Object.keys(infoOfModelName).map(modelName => {
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
const {
downloadable,
cost,
supportsTools,
supportsFIM,
reasoningCapabilities,
contextWindow,
isUnrecognizedModel,
maxOutputTokens,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName)
const removeModelButton = <button
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
>
<X className="w-3.5 h-3.5" />
</button>
return (
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<td className="py-2 px-3 relative">
{!showAsDefault && removeModelButton}
{modelName}
</td>
<td className="py-2 px-3">${cost.output ?? ''}</td>
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
<td className="py-2 px-3"><YesNoText val={true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsTools || null} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
)
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true} />
</td>
<td colSpan={4}></td>
</tr>
</tbody>
</table>
}
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
const VoidOnboarding = () => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const isOnboardingComplete = false // voidSettingsService._isOnboardingComplete
if (isOnboardingComplete) {
return null
}
const [pageIndex, setPageIndex] = useState(0)
const skipButton = <SkipButton onClick={() => { setPageIndex(pageIndex + 1) }} />
// page 1 state
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
// page 2 state
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(null)
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
private: ['ollama', 'vLLM', 'openAICompatible'],
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
all: providerNames,
// TODO allow user to redo onboarding
}
const didFillInProviderSettings = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName]._didFillInProviderSettings
const isApiKeyLongEnoughIfApiKeyExists = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].apiKey ? voidSettingsState.settingsOfProvider[selectedProviderName].apiKey.length > 15 : true
const isAtLeastOneModel = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].models.length >= 1
const didFillInSelectedProviderSettings = !!(didFillInProviderSettings && isApiKeyLongEnoughIfApiKeyExists && isAtLeastOneModel)
const prevAndNextButtons = <div className="self-end flex items-center gap-1 pb-8">
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
<NextButton
onClick={() => { setPageIndex(pageIndex + 1) }}
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
/>
</div>
// cannot be md
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Models with the best performance on benchmarks.",
private: "Fully private and hosted on your computer/network.",
cheap: "Free and affordable options.",
all: "",
}
// can be md
const detailedDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Most intelligent and best for agent mode.",
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:founders@voideditor.com) for help setting up at your company.",
cheap: "Great deals like Gemini 2.5 Pro or self-host a model for free.",
all: "",
}
// set the selected provider name appropriately
useEffect(() => {
if (wantToUseOption && providerNamesOfWantToUseOption[wantToUseOption].length > 0) {
setSelectedProviderName(providerNamesOfWantToUseOption[wantToUseOption][0]);
} else {
setSelectedProviderName(null);
}
}, [wantToUseOption]);
// set wantToUseOption to smart when page changes
useEffect(() => {
setWantToUseOption(wantToUseOption);
}, [pageIndex]);
// TODO add a description next to the skip button saying (you can always restart the onboarding in Settings)
const contentOfIdx: { [pageIndex: number]: React.ReactNode } = {
0: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn >
<div className="text-5xl font-light mb-6 mt-12 text-center">Welcome to Void</div>
<div className="w-8 h-8 mb-2">
<VoidImage className='h-full w-full' />
</div>
</FadeIn>
<FadeIn delayMs={1000} className="text-center pb-8" onClick={() => { setPageIndex(pageIndex + 1) }}>
Get Started
</FadeIn>
</div>,
1: <div className="max-w-full w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn>
<div className="text-3xl font-medium mb-6 mt-8 text-center">AI Preferences</div>
<div className="flex flex-col items-center w-full mx-auto">
<div className="text-base text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 w-full md:max-w-[75%] max-w-[90%]">
<div
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5"
>
<span className="text-5xl mb-4">🧠</span>
<h3 className="text-xl font-medium mb-3">Intelligence</h3>
<p className="text-center text-sm text-void-fg-2">{basicDescOfWantToUseOption['smart']}</p>
</div>
<div
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5"
>
<span className="text-5xl mb-4">🔒</span>
<h3 className="text-xl font-medium mb-3">Privacy</h3>
<p className="text-center text-sm text-void-fg-2">{basicDescOfWantToUseOption['private']}</p>
</div>
<div
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5"
>
<span className="text-5xl mb-4">💵</span>
<h3 className="text-xl font-medium mb-3">Low-Cost</h3>
<p className="text-center text-sm text-void-fg-2">{basicDescOfWantToUseOption['cheap']}</p>
</div>
</div>
</div>
</FadeIn>
<div className="max-w-[600px] w-full flex flex-col items-center justify-between">
{prevAndNextButtons}
</div>
</div>,
2: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn className="flex flex-col gap-2 w-full">
<div className="text-5xl font-light mb-6 mt-12 text-center">Choose a Provider</div>
<div className="mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md">
<button
onClick={() => {
setWantToUseOption('smart');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'smart'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Intelligent
</button>
<button
onClick={() => {
setWantToUseOption('private');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'private'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Private
</button>
<button
onClick={() => {
setWantToUseOption('cheap');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'cheap'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Low-Cost
</button>
<button
onClick={() => {
setWantToUseOption('all')
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'all'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
All
</button>
</div>
{/* Provider Buttons */}
<div
key={wantToUseOption}
className="flex flex-wrap items-center mt-4 min-h-[37px] w-full"
>
{(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => {
const isSelected = selectedProviderName === providerName
return (
<button
key={providerName}
onClick={() => setSelectedProviderName(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-colors duration-150 border
${isSelected ? 'bg-[#0e70c0] text-white shadow-sm border-[#0e70c0]/80' : 'bg-[#0e70c0]/10 text-void-fg-3 hover:bg-[#0e70c0]/30 border-[#0e70c0]/20'}
`}
>
{displayInfoOfProviderName(providerName).title}
</button>
)
})}
</div>
{/* Description */}
<div className="text-left text-sm text-void-fg-3 px-2 py-1">
<div className='pl-4 select-text'>
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
</div>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4'>
{/* Models Table */}
<TableOfModelsForProvider providerName={selectedProviderName} />
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8'>
<div className='text-base font-semibold'>
Add {displayInfoOfProviderName(selectedProviderName).title}
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
</div>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
{/* Button and status indicators */}
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <div className="mt-2"><AnimatedCheckmarkButton text='Added' /></div>}
</div>
</div>}
</FadeIn>
{prevAndNextButtons}
</div>,
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
// <FadeIn>
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
// </div>
// </FadeIn>
// {prevAndNextButtons}
// </div>,
3: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn>
<div className="text-5xl font-light mb-6 mt-12">Settings and Themes</div>
<div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
<h4 className="text-void-fg-3 mb-2">Transfer your settings from an existing editor?</h4>
<OneClickSwitchButton fromEditor="VS Code" />
<OneClickSwitchButton fromEditor="Cursor" />
<OneClickSwitchButton fromEditor="Windsurf" />
</div>
</FadeIn>
{prevAndNextButtons}
</div>,
4: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn className="text-5xl font-light mb-6 mt-12">
Jump in
</FadeIn>
<FadeIn className="text-center">
Enter the Void
</FadeIn>
{prevAndNextButtons}
</div>,
}
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-auto flex flex-col items-center justify-between">
{contentOfIdx[pageIndex]}
</div>
}
@@ -48,7 +48,7 @@ export const VoidTooltip = () => {
font-size: 12px;
padding: 0px 8px;
border-radius: 6px;
z-index: 999;
z-index: 999999;
}
#void-tooltip {
@@ -11,6 +11,7 @@ export default defineConfig({
'./src2/sidebar-tsx/index.tsx',
'./src2/void-settings-tsx/index.tsx',
'./src2/void-tooltip/index.tsx',
'./src2/void-onboarding/index.tsx',
'./src2/quick-edit-tsx/index.tsx',
'./src2/diff/index.tsx',
],
@@ -1,44 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
export interface ISearchReplaceService {
readonly _serviceBrand: undefined;
}
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
export class SearchReplaceService extends Disposable implements ISearchReplaceService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
constructor(
// @ILLMMessageService private readonly llmMessageService: ILLMMessageService,
) {
super()
}
// send(params: ServiceSendLLMMessageParams & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
// this.llmMessageService.sendLLMMessage({
// ...params as ServiceSendLLMMessageParams,
// onText: (p) => {
// const { retry } = params.onText(p)
// if (retry) {
// }
// }
// })
// }
}
registerSingleton(ISearchReplaceService, SearchReplaceService, InstantiationType.Eager);
@@ -25,10 +25,37 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
import { localize2 } from '../../../../nls.js';
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
import { IChatThreadService } from './chatThreadService.js';
import { getActiveWindow } from '../../../../base/browser/dom.js';
// ---------- Register commands and keybindings ----------
const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
if (!currentSelections) return null
for (let i = 0; i < currentSelections.length; i += 1) {
const s = currentSelections[i]
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
if (s.type === 'File' && newSelection.type === 'File') {
return i
}
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
// if there's any collision return true
const [oldStart, oldEnd] = s.range
const [newStart, newEnd] = newSelection.range
if (oldStart !== newStart || oldEnd !== newEnd) continue
return i
}
if (s.type === 'Folder' && newSelection.type === 'Folder') {
return i
}
}
return null
}
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
if (!range)
return null
@@ -63,31 +90,6 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e
// }
const findStagingItemToReplace = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): [number, StagingSelectionItem] | null => {
if (!currentSelections) return null
for (let i = 0; i < currentSelections.length; i += 1) {
const s = currentSelections[i]
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
if (s.type === 'File' && newSelection.type === 'File') {
return [i, s] as const
}
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
// if there's any collision return true
const [oldStart, oldEnd] = s.range
const [newStart, newEnd] = newSelection.range
if (oldStart !== newStart || oldEnd !== newEnd) continue
return [i, s] as const
}
if (s.type === 'Folder' && newSelection.type === 'Folder') {
return [i, s] as const
}
}
return null
}
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
registerAction2(class extends Action2 {
@@ -132,7 +134,7 @@ registerAction2(class extends Action2 {
}
const selection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
@@ -163,21 +165,17 @@ registerAction2(class extends Action2 {
}
// if matches with existing selection, overwrite (since text may change)
const replaceRes = findStagingItemToReplace(selections, selection)
if (replaceRes) {
const [idx, newSel] = replaceRes
if (idx !== undefined && idx !== -1) {
setSelections([
...selections!.slice(0, idx),
newSel,
...selections!.slice(idx + 1, Infinity)
])
}
const idx = findStagingSelectionIndex(selections, newSelection)
if (idx !== null && idx !== -1) {
setSelections([
...selections!.slice(0, idx),
newSelection,
...selections!.slice(idx + 1, Infinity)
])
}
// if no match, add it
else {
setSelections([...(selections ?? []), selection])
setSelections([...(selections ?? []), newSelection])
}
}
@@ -228,9 +226,13 @@ registerAction2(class extends Action2 {
metricsService.capture('Chat Navigation', { type: 'New Chat' })
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
// focus
stateService.fireFocusChat()
const window = getActiveWindow()
window.requestAnimationFrame(() => stateService.fireFocusChat())
}
})
@@ -14,10 +14,11 @@ import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { basename } from '../../../../base/common/path.js'
import { IVoidCommandBarService } from './voidCommandBarService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
import { IMarkerService } from '../../../../platform/markers/common/markers.js'
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { ToolName } from '../common/prompt/prompts.js'
import { IVoidSettingsService } from '../common/voidSettingsService.js'
// tool use for AI
@@ -25,7 +26,7 @@ import { ToolName } from '../common/prompt/prompts.js'
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => Promise<ToolCallParams[T]> }
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
@@ -33,7 +34,7 @@ type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awai
// pagination info
export const MAX_FILE_CHARS_PAGE = 50_000
export const MAX_FILE_CHARS_PAGE = Infinity
export const MAX_CHILDREN_URIs_PAGE = 500
export const MAX_TERMINAL_CHARS_PAGE = 20_000
export const TERMINAL_TIMEOUT_TIME = 5 // seconds
@@ -151,34 +152,38 @@ export class ToolsService implements IToolsService {
@IVoidCommandBarService private readonly commandBarService: IVoidCommandBarService,
@IDirectoryStrService private readonly directoryStrService: IDirectoryStrService,
@IMarkerService private readonly markerService: IMarkerService,
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
) {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.validateParams = {
read_file: async (params: RawToolParamsObj) => {
read_file: (params: RawToolParamsObj) => {
const { uri: uriStr, start_line: startLineUnknown, end_line: endLineUnknown, page_number: pageNumberUnknown } = params
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
const startLine = validateNumber(startLineUnknown, { default: null })
const endLine = validateNumber(endLineUnknown, { default: null })
let startLine = validateNumber(startLineUnknown, { default: null })
let endLine = validateNumber(endLineUnknown, { default: null })
if (startLine !== null && startLine < 1) startLine = null
if (endLine !== null && endLine < 1) endLine = null
return { uri, startLine, endLine, pageNumber }
},
ls_dir: async (params: RawToolParamsObj) => {
ls_dir: (params: RawToolParamsObj) => {
const { uri: uriStr, page_number: pageNumberUnknown } = params
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
return { rootURI: uri, pageNumber }
},
get_dir_structure: async (params: RawToolParamsObj) => {
get_dir_structure: (params: RawToolParamsObj) => {
const { uri: uriStr, } = params
const uri = validateURI(uriStr)
return { rootURI: uri }
},
search_pathnames_only: async (params: RawToolParamsObj) => {
search_pathnames_only: (params: RawToolParamsObj) => {
const {
query: queryUnknown,
search_in_folder: includeUnknown,
@@ -192,7 +197,7 @@ export class ToolsService implements IToolsService {
return { queryStr, searchInFolder, pageNumber }
},
search_files: async (params: RawToolParamsObj) => {
search_for_files: (params: RawToolParamsObj) => {
const {
query: queryUnknown,
search_in_folder: searchInFolderUnknown,
@@ -209,9 +214,17 @@ export class ToolsService implements IToolsService {
return { queryStr, searchInFolder, isRegex, pageNumber }
},
read_lint_errors: (params: RawToolParamsObj) => {
const {
uri: uriUnknown,
} = params
const uri = validateURI(uriUnknown)
return { uri }
},
// ---
create_file_or_folder: async (params: RawToolParamsObj) => {
create_file_or_folder: (params: RawToolParamsObj) => {
const { uri: uriUnknown } = params
const uri = validateURI(uriUnknown)
const uriStr = validateStr('uri', uriUnknown)
@@ -219,7 +232,7 @@ export class ToolsService implements IToolsService {
return { uri, isFolder }
},
delete_file_or_folder: async (params: RawToolParamsObj) => {
delete_file_or_folder: (params: RawToolParamsObj) => {
const { uri: uriUnknown, params: paramsStr } = params
const uri = validateURI(uriUnknown)
const isRecursive = validateRecursiveParamStr(paramsStr)
@@ -228,14 +241,14 @@ export class ToolsService implements IToolsService {
return { uri, isRecursive, isFolder }
},
edit_file: async (params: RawToolParamsObj) => {
edit_file: (params: RawToolParamsObj) => {
const { uri: uriStr, change_description: changeDescriptionUnknown } = params
const uri = validateURI(uriStr)
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
return { uri, changeDescription }
},
run_terminal_command: async (params: RawToolParamsObj) => {
run_terminal_command: (params: RawToolParamsObj) => {
const { command: commandUnknown, terminal_id: terminalIdUnknown, wait_for_completion: waitForCompletionUnknown } = params
const command = validateStr('command', commandUnknown)
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
@@ -266,8 +279,8 @@ export class ToolsService implements IToolsService {
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (contents.length - 1) - toIdx >= 1
return { result: { fileContents, hasNextPage } }
const totalFileLen = contents.length
return { result: { fileContents, totalFileLen, hasNextPage } }
},
ls_dir: async ({ rootURI, pageNumber }) => {
@@ -298,7 +311,7 @@ export class ToolsService implements IToolsService {
return { result: { uris, hasNextPage } }
},
search_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
search_for_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
const searchFolders = searchInFolder === null ?
workspaceContextService.getWorkspace().folders.map(f => f.uri)
: [searchInFolder]
@@ -320,6 +333,12 @@ export class ToolsService implements IToolsService {
return { result: { queryStr, uris, hasNextPage } }
},
read_lint_errors: async ({ uri }) => {
await timeout(1000)
const { lintErrors } = this._getLintErrors(uri)
return { result: { lintErrors } }
},
// ---
create_file_or_folder: async ({ uri, isFolder }) => {
@@ -357,20 +376,11 @@ export class ToolsService implements IToolsService {
editCodeService.interruptURIStreaming({ uri: diffZoneURI })
}
// at end, get lint errors
const lintErrorsPromise = applyDonePromise.then(async () => {
await timeout(500)
const lintErrors = this.markerService
.read({ resource: uri })
.map(l => ({
code: typeof l.code === 'string' ? l.code : l.code?.value || '',
message: l.message,
startLineNumber: l.startLineNumber,
endLineNumber: l.endLineNumber,
} satisfies LintErrorItem))
if (!lintErrors.length) return { lintErrors: null }
return { lintErrors, }
await timeout(2000)
const { lintErrors } = this._getLintErrors(uri)
return { lintErrors }
})
return { result: lintErrorsPromise, interruptTool }
@@ -384,12 +394,16 @@ export class ToolsService implements IToolsService {
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
const lintErrorsStr = (lintErrors: LintErrorItem[]) => lintErrors.map((e, i) => `Error ${i + 1}:\nLines Affected: ${e.startLineNumber}-${e.endLineNumber}\nError message:${e.message}`).join('\n\n')
const stringifyLintErrors = (lintErrors: LintErrorItem[]) => {
return lintErrors
.map((e, i) => `Error ${i + 1}:\nLines Affected: ${e.startLineNumber}-${e.endLineNumber}\nError message:${e.message}`)
.join('\n\n')
}
// given to the LLM after the call
this.stringOfResult = {
read_file: (params, result) => {
return result.fileContents + nextPageStr(result.hasNextPage)
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
},
ls_dir: (params, result) => {
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
@@ -401,9 +415,14 @@ export class ToolsService implements IToolsService {
search_pathnames_only: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
search_files: (params, result) => {
search_for_files: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
read_lint_errors: (params, result) => {
return result.lintErrors ?
stringifyLintErrors(result.lintErrors)
: 'No lint errors found.'
},
// ---
create_file_or_folder: (params, result) => {
return `URI ${params.uri.fsPath} successfully created.`
@@ -412,10 +431,13 @@ export class ToolsService implements IToolsService {
return `URI ${params.uri.fsPath} successfully deleted.`
},
edit_file: (params, result) => {
const lintErrsString = (
this.voidSettingsService.state.globalSettings.includeToolLintErrors ?
(result.lintErrors ? ` Lint errors found after change:\n${stringifyLintErrors(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.`
: ` No lint errors found.`)
: '')
const additionalStr = result.lintErrors ? `Lint errors found after change:\n${lintErrorsStr(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` : `No lint errors found.`
return `Change successfully made to ${params.uri.fsPath}.${additionalStr}`
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
},
run_terminal_command: (params, result) => {
const {
@@ -449,6 +471,22 @@ export class ToolsService implements IToolsService {
}
private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } {
const lintErrors = this.markerService
.read({ resource: uri })
.filter(l => l.severity === MarkerSeverity.Error || l.severity === MarkerSeverity.Warning)
.map(l => ({
code: typeof l.code === 'string' ? l.code : l.code?.value || '',
message: (l.severity === MarkerSeverity.Error ? '(error) ' : '(warning) ') + l.message,
startLineNumber: l.startLineNumber,
endLineNumber: l.endLineNumber,
} satisfies LintErrorItem))
if (!lintErrors.length) return { lintErrors: null }
return { lintErrors, }
}
}
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
@@ -52,6 +52,9 @@ import './voidSelectionHelperWidget.js'
// register tooltip service
import './tooltipService.js'
// register onboarding service
import './voidOnboardingService.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
@@ -0,0 +1,52 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { mountVoidOnboarding } from './react/out/void-onboarding/index.js'
import { h, getActiveWindow } from '../../../../base/browser/dom.js';
// Onboarding contribution that mounts the component at startup
export class OnboardingContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.voidOnboarding';
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.initialize();
}
private initialize(): void {
// Get the active window reference for multi-window support
const targetWindow = getActiveWindow();
// Find the monaco-workbench element using the proper window reference
const workbench = targetWindow.document.querySelector('.monaco-workbench');
if (workbench) {
const onboardingContainer = h('div.void-onboarding-container').root;
workbench.appendChild(onboardingContainer);
this.instantiationService.invokeFunction((accessor: ServicesAccessor) => {
const result = mountVoidOnboarding(onboardingContainer, accessor);
if (result && typeof result.dispose === 'function') {
this._register(toDisposable(result.dispose));
}
});
// Register cleanup for the DOM element
this._register(toDisposable(() => {
if (onboardingContainer.parentElement) {
onboardingContainer.parentElement.removeChild(onboardingContainer);
}
}));
}
}
}
// Register the contribution to be initialized during the AfterRestored phase
registerWorkbenchContribution2(OnboardingContribution.ID, OnboardingContribution, WorkbenchPhase.AfterRestored);
@@ -8,45 +8,94 @@ import Severity from '../../../../base/common/severity.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { localize2 } from '../../../../nls.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { INotificationActions, INotificationService } from '../../../../platform/notification/common/notification.js';
import { IMetricsService } from '../common/metricsService.js';
import { IVoidUpdateService } from '../common/voidUpdateService.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import * as dom from '../../../../base/browser/dom.js';
import { IUpdateService } from '../../../../platform/update/common/update.js';
import { VoidCheckUpdateRespose } from '../common/voidUpdateServiceTypes.js';
import { IAction } from '../../../../base/common/actions.js';
const notifyYesUpdate = (notifService: INotificationService, res: { message?: string } = {}) => {
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService) => {
const message = res?.message || 'This is a very old version of Void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
const notifController = notifService.notify({
severity: Severity.Info,
message: message,
sticky: true,
progress: { worked: 0, total: 100 },
actions: {
primary: [{
id: 'void.updater.update',
enabled: true,
let actions: INotificationActions | undefined
if (res?.action) {
const primary: IAction[] = []
if (res.action === 'reinstall') {
primary.push({
label: `Reinstall`,
id: 'void.updater.reinstall',
enabled: true,
tooltip: '',
class: undefined,
run: () => {
const { window } = dom.getActiveWindow()
window.open('https://voideditor.com/download-beta')
}
},
{
id: 'void.updater.site',
})
}
if (res.action === 'download') {
primary.push({
label: `Download`,
id: 'void.updater.download',
enabled: true,
label: `Void Site`,
tooltip: '',
class: undefined,
run: () => {
const { window } = dom.getActiveWindow()
window.open('https://voideditor.com/')
updateService.downloadUpdate()
}
}],
})
}
if (res.action === 'apply') {
primary.push({
label: `Apply`,
id: 'void.updater.apply',
enabled: true,
tooltip: '',
class: undefined,
run: () => {
updateService.applyUpdate()
}
})
}
if (res.action === 'restart') {
primary.push({
label: `Restart`,
id: 'void.updater.restart',
enabled: true,
tooltip: '',
class: undefined,
run: () => {
updateService.quitAndInstall()
}
})
}
primary.push({
id: 'void.updater.site',
enabled: true,
label: `Void Site`,
tooltip: '',
class: undefined,
run: () => {
const { window } = dom.getActiveWindow()
window.open('https://voideditor.com/')
}
})
actions = {
primary: primary,
secondary: [{
id: 'void.updater.close',
enabled: true,
@@ -57,19 +106,24 @@ const notifyYesUpdate = (notifService: INotificationService, res: { message?: st
notifController.close()
}
}]
},
}
}
else {
actions = undefined
}
const notifController = notifService.notify({
severity: Severity.Info,
message: message,
sticky: true,
progress: actions ? { worked: 0, total: 100 } : undefined,
actions: actions,
})
// const d = notifController.onDidClose(() => {
// notifyYesUpdate(notifService, res)
// d.dispose()
// })
}
const notifyNoUpdate = (notifService: INotificationService) => {
notifService.notify({
severity: Severity.Info,
message: 'Void is up-to-date!',
})
}
const notifyErrChecking = (notifService: INotificationService) => {
const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!`
notifService.notify({
@@ -80,6 +134,34 @@ const notifyErrChecking = (notifService: INotificationService) => {
}
const performVoidCheck = async (
explicit: boolean,
notifService: INotificationService,
voidUpdateService: IVoidUpdateService,
metricsService: IMetricsService,
updateService: IUpdateService,
) => {
const metricsTag = explicit ? 'Manual' : 'Auto'
metricsService.capture(`Void Update ${metricsTag}: Checking...`, {})
const res = await voidUpdateService.check(explicit)
if (!res) {
notifyErrChecking(notifService);
metricsService.capture(`Void Update ${metricsTag}: Error`, { res })
}
else {
if (res.message) {
notifyUpdate(res, notifService, updateService)
metricsService.capture(`Void Update ${metricsTag}: Yes`, { res })
}
else {
metricsService.capture(`Void Update ${metricsTag}: No`, { res })
return
}
}
}
// Action
registerAction2(class extends Action2 {
@@ -94,12 +176,8 @@ registerAction2(class extends Action2 {
const voidUpdateService = accessor.get(IVoidUpdateService)
const notifService = accessor.get(INotificationService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Void Update Manual: Checking...', {})
const res = await voidUpdateService.check(true)
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Manual: Error', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(notifService, res); metricsService.capture('Void Update Manual: Yes', { res }) }
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update Manual: No', { res }) }
const updateService = accessor.get(IUpdateService)
performVoidCheck(true, notifService, voidUpdateService, metricsService, updateService)
}
})
@@ -107,17 +185,15 @@ registerAction2(class extends Action2 {
class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.void.voidUpdate'
constructor(
@IVoidUpdateService private readonly voidUpdateService: IVoidUpdateService,
@IMetricsService private readonly metricsService: IMetricsService,
@INotificationService private readonly notifService: INotificationService,
@IVoidUpdateService voidUpdateService: IVoidUpdateService,
@IMetricsService metricsService: IMetricsService,
@INotificationService notifService: INotificationService,
@IUpdateService updateService: IUpdateService,
) {
super()
const autoCheck = async () => {
this.metricsService.capture('Void Update Startup: Checking...', {})
const res = await this.voidUpdateService.check(false)
if (!res) { notifyErrChecking(this.notifService); this.metricsService.capture('Void Update Startup: Error', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res); this.metricsService.capture('Void Update Startup: Yes', { res }) }
else if (!res.hasUpdate) { this.metricsService.capture('Void Update Startup: No', { res }) } // display nothing if up to date
const autoCheck = () => {
performVoidCheck(false, notifService, voidUpdateService, metricsService, updateService)
}
// check once 5 seconds after mount
@@ -6,15 +6,17 @@
import { URI } from '../../../../base/common/uri.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { ToolName } from './prompt/prompts.js';
import { AnthropicReasoning, RawToolCallObj } from './sendLLMMessageTypes.js';
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
import { ToolCallParams, ToolResultType } 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;
} & (
// in order of events:
| { type: 'invalid_params', result: null, name: T, params: RawToolCallObj | null, }
| { type: 'invalid_params', result: null, name: T, }
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
@@ -22,7 +24,7 @@ export type ToolMessage<T extends ToolName> = {
| { 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: 'rejected', result: null, name: T, params: ToolCallParams[T] }
) // user rejected
export type DecorativeCanceledTool = {
@@ -58,7 +60,6 @@ export type ChatMessage =
role: 'assistant';
displayContent: string; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string; // reasoning from the LLM, used for step-by-step thinking
toolCall: RawToolCallObj | undefined;
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
}
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
class SurroundingsRemover {
export class SurroundingsRemover {
readonly originalS: string
i: number
j: number
@@ -58,12 +58,13 @@ class SurroundingsRemover {
// return offset === suffix.length
// }
// either removes all or nothing
removeFromStartUntilFullMatch = (until: string, alsoRemoveUntilStr: boolean) => {
const index = this.originalS.indexOf(until, this.i)
if (index === -1) {
this.i = this.j + 1
return null
// this.i = this.j + 1
return false
}
// console.log('index', index, until.length)
@@ -39,8 +39,11 @@ export const defaultProviderSettings = {
apiKey: '',
},
xAI: {
apiKey: ''
apiKey: '',
},
mistral: {
apiKey: '',
}
} as const
@@ -48,11 +51,14 @@ export const defaultProviderSettings = {
export const defaultModelsOfProvider = {
openAI: [ // https://platform.openai.com/docs/models/gp
'gpt-4.1',
'gpt-4.1-mini',
'gpt-4.1-nano',
'o3-mini',
'o1',
'o1-mini',
'gpt-4o',
'gpt-4o-mini',
// 'o1',
// 'o1-mini',
// 'gpt-4o',
// 'gpt-4o-mini',
],
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
'claude-3-7-sonnet-latest',
@@ -78,15 +84,17 @@ export const defaultModelsOfProvider = {
vLLM: [ // autodetected
],
openRouter: [ // https://openrouter.ai/models
'anthropic/claude-3.7-sonnet:thinking',
// 'anthropic/claude-3.7-sonnet:thinking',
'anthropic/claude-3.7-sonnet',
'anthropic/claude-3.5-sonnet',
'deepseek/deepseek-r1',
'deepseek/deepseek-r1-zero:free',
'mistralai/codestral-2501',
'qwen/qwen-2.5-coder-32b-instruct',
'openrouter/quasar-alpha',
'google/gemini-2.5-pro-preview-03-25',
// 'mistralai/codestral-2501',
// 'qwen/qwen-2.5-coder-32b-instruct',
// 'mistralai/mistral-small-3.1-24b-instruct:free',
'google/gemini-2.0-flash-lite-preview-02-05:free',
// 'google/gemini-2.0-flash-lite-preview-02-05:free',
// 'google/gemini-2.0-pro-exp-02-05:free',
// 'google/gemini-2.0-flash-exp:free',
],
@@ -96,13 +104,12 @@ export const defaultModelsOfProvider = {
'llama-3.1-8b-instant',
// 'qwen-2.5-coder-32b', // preview mode (experimental)
],
// not supporting mistral right now- it's last on Void usage, and a huge pain to set up since it's nonstandard (it supports codestral FIM but it's on v1/fim/completions, etc)
// mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
// 'codestral-latest',
// 'mistral-large-latest',
// 'ministral-3b-latest',
// 'ministral-8b-latest',
// ],
mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
'codestral-latest',
'mistral-large-latest',
'ministral-3b-latest',
'ministral-8b-latest',
],
openAICompatible: [], // fallback
} as const satisfies Record<ProviderName, string[]>
@@ -122,8 +129,8 @@ export type VoidStaticModelInfo = { // not stateful
sizeGb: number | 'not-known'
}
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special parameter
supportsTools?: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style';
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special paramete
specialToolFormat?: 'openai-style' | 'anthropic-style', // null defaults to XML
supportsFIM: boolean;
reasoningCapabilities: false | {
@@ -169,12 +176,9 @@ const modelOptionsDefaults: VoidStaticModelInfo = {
reasoningCapabilities: false,
}
// TODO!!! double check all context sizes below
// TODO!!! add openrouter common models
// TODO!!! allow user to modify capabilities and tell them if autodetected model or falling back
const openSourceModelOptions_assumingOAICompat = {
'deepseekR1': {
supportsFIM: false,
@@ -286,6 +290,12 @@ const openSourceModelOptions_assumingOAICompat = {
contextWindow: 128_000, maxOutputTokens: 8_192,
},
'quasar': { // openrouter/quasar-alpha
supportsFIM: false,
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
contextWindow: 1_000_000, maxOutputTokens: 32_000,
}
} as const satisfies { [s: string]: Partial<VoidStaticModelInfo> }
@@ -306,9 +316,6 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (
...fallbackKnownValues
}
}
if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower))
return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat])
if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions['gemini-2.5-pro-exp-03-25'])
if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022'])
@@ -339,12 +346,21 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (
if (lower.includes('openhands')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['openhands-lm-32b'], }) // max output unclear
if (lower.includes('quasar') || lower.includes('quaser')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['quasar'] })
if (lower.includes('gpt') && lower.includes('mini') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1-mini'])
if (lower.includes('gpt') && lower.includes('nano') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1-nano'])
if (lower.includes('gpt') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1'])
if (lower.includes('4o') && lower.includes('mini')) return toFallback(openAIModelOptions['gpt-4o-mini'])
if (lower.includes('4o')) return toFallback(openAIModelOptions['gpt-4o'])
if (lower.includes('o1') && lower.includes('mini')) return toFallback(openAIModelOptions['o1-mini'])
if (lower.includes('o1')) return toFallback(openAIModelOptions['o1'])
if (lower.includes('o3') && lower.includes('mini')) return toFallback(openAIModelOptions['o3-mini'])
// if (lower.includes('o3')) return toFallback(openAIModelOptions['o3'])
if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower))
return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat])
return toFallback(modelOptionsDefaults)
}
@@ -362,6 +378,7 @@ const anthropicModelOptions = {
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: {
supportsReasoning: true,
@@ -370,6 +387,7 @@ const anthropicModelOptions = {
reasoningMaxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
},
},
'claude-3-5-sonnet-20241022': {
contextWindow: 200_000,
@@ -377,6 +395,7 @@ const anthropicModelOptions = {
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: false,
},
@@ -386,6 +405,7 @@ const anthropicModelOptions = {
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: false,
},
@@ -395,6 +415,7 @@ const anthropicModelOptions = {
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: false,
},
@@ -403,6 +424,7 @@ const anthropicModelOptions = {
downloadable: false,
maxOutputTokens: 4_096,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: false,
}
@@ -436,6 +458,36 @@ const anthropicSettings: VoidStaticProviderInfo = {
// ---------------- OPENAI ----------------
const openAIModelOptions = { // https://platform.openai.com/docs/pricing
'gpt-4.1': {
contextWindow: 1_047_576,
maxOutputTokens: 32_768,
cost: { input: 2.00, output: 8.00, cache_read: 0.50 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role',
reasoningCapabilities: false,
},
'gpt-4.1-mini': {
contextWindow: 1_047_576,
maxOutputTokens: 32_768,
cost: { input: 0.40, output: 1.60, cache_read: 0.10 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role',
reasoningCapabilities: false,
},
'gpt-4.1-nano': {
contextWindow: 1_047_576,
maxOutputTokens: 32_768,
cost: { input: 0.10, output: 0.40, cache_read: 0.03 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role',
reasoningCapabilities: false,
},
'o1': {
contextWindow: 128_000,
maxOutputTokens: 100_000,
@@ -460,6 +512,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
@@ -478,6 +531,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'system-role', // ??
reasoningCapabilities: false,
},
@@ -508,6 +562,15 @@ const xAIModelOptions = {
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
// 'grok-3': {
// contextWindow: 1_000_000,
// maxOutputTokens: null,
// cost: {},
// downloadable: false,
// supportsFIM: false,
// supportsSystemMessage: 'system-role',
// reasoningCapabilities: {canIOReasoning:false, canTurnOffReasoning:true,},
// }
} as const satisfies { [s: string]: VoidStaticModelInfo }
const xAISettings: VoidStaticProviderInfo = {
@@ -615,6 +678,55 @@ const deepseekSettings: VoidStaticProviderInfo = {
modelOptionsFallback: (modelName) => { return null }
}
// ---------------- MISTRAL ----------------
const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#pricing https://docs.mistral.ai/getting-started/models/models_overview/#premier-models
'mistral-large-latest': {
contextWindow: 131_000,
maxOutputTokens: 8_192,
cost: { input: 2.00, output: 6.00 },
supportsFIM: false,
downloadable: { sizeGb: 73 },
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'codestral-latest': {
contextWindow: 256_000,
maxOutputTokens: 8_192,
cost: { input: 0.30, output: 0.90 },
supportsFIM: true,
downloadable: { sizeGb: 13 },
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'ministral-8b-latest': { // ollama 'mistral'
contextWindow: 131_000,
maxOutputTokens: 4_096,
cost: { input: 0.10, output: 0.10 },
supportsFIM: false,
downloadable: { sizeGb: 4.1 },
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'ministral-3b-latest': {
contextWindow: 131_000,
maxOutputTokens: 4_096,
cost: { input: 0.04, output: 0.04 },
supportsFIM: false,
downloadable: { sizeGb: 'not-known' },
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
} as const satisfies { [s: string]: VoidStaticModelInfo }
const mistralSettings: VoidStaticProviderInfo = {
modelOptions: mistralModelOptions,
modelOptionsFallback: (modelName) => { return null },
}
// ---------------- GROQ ----------------
const groqModelOptions = { // https://console.groq.com/docs/models, https://groq.com/pricing/
'llama-3.3-70b-versatile': {
@@ -678,7 +790,6 @@ const ollamaModelOptions = {
downloadable: { sizeGb: 1.9 },
supportsFIM: true,
supportsSystemMessage: 'system-role',
supportsTools: false,
reasoningCapabilities: false,
},
'qwen2.5-coder': {
@@ -688,7 +799,6 @@ const ollamaModelOptions = {
downloadable: { sizeGb: 4.7 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: false,
reasoningCapabilities: false,
},
'qwq': {
@@ -698,7 +808,6 @@ const ollamaModelOptions = {
downloadable: { sizeGb: 20 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'TODO-yes-but-we-handle-it-manually',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
},
'deepseek-r1': {
@@ -708,7 +817,6 @@ const ollamaModelOptions = {
downloadable: { sizeGb: 4.7 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'TODO-yes-but-we-handle-it-manually',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
},
@@ -825,7 +933,6 @@ const openRouterModelOptions_assumingOpenAICompat = {
maxOutputTokens: null,
cost: { input: 0.3, output: 0.9 },
downloadable: false,
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'qwen/qwen-2.5-coder-32b-instruct': {
@@ -886,8 +993,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
vLLM: vLLMSettings,
ollama: ollamaSettings,
openAICompatible: openaiCompatible,
// TODO!!!
mistral: mistralSettings,
// googleVertex: {},
// microsoftAzure: {},
// openHands: {},
@@ -947,6 +1053,14 @@ export const getIsReasoningEnabledState = (
}
export const getMaxOutputTokens = (providerName: ProviderName, modelName: string, opts: { isReasoningEnabled: boolean }) => {
const {
reasoningCapabilities,
maxOutputTokens
} = getModelCapabilities(providerName, modelName)
return opts.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
}
// used to force reasoning state (complex) into something simple we can just read from when sending a message
export const getSendableReasoningInfo = (
featureName: FeatureName,
@@ -6,17 +6,22 @@
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolCallObj } from '../sendLLMMessageTypes.js';
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { ChatMode } from '../voidSettingsTypes.js';
// this is just for ease of readability
// Triple backtick wrapper used throughout the prompts for code blocks
export const tripleTick = ['```', '```']
// Maximum limits for directory structure information
export const MAX_DIRSTR_CHARS_TOTAL_BEGINNING = 20_000
export const MAX_DIRSTR_CHARS_TOTAL_TOOL = 20_000
export const MAX_DIRSTR_RESULTS_TOTAL_BEGINNING = 100
export const MAX_DIRSTR_RESULTS_TOTAL_TOOL = 100
// Maximum character limits for prefix and suffix context
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
@@ -66,11 +71,11 @@ export const voidTools = {
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI.`,
description: `Returns full contents of a given file.`,
params: {
...uriParam('file'),
start_line: { description: 'Optional. Default is 1. Start reading on this line.' },
end_line: { description: 'Optional. Default is Infinity. Stop reading after this line.' },
start_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to 1.' },
end_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to Infinity.' },
...paginationParam,
},
},
@@ -79,7 +84,7 @@ export const voidTools = {
name: 'ls_dir',
description: `Lists all files and folders in the given URI.`,
params: {
...uriParam('folder'),
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
...paginationParam,
},
},
@@ -101,29 +106,37 @@ export const voidTools = {
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.` },
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
...paginationParam,
},
},
search_files: {
name: 'search_files',
description: `Returns all pathnames that match a given query (searches ONLY file contents). The query can be any substring or glob. You can follow this with read_file to view result contents.`,
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. Only search files in this given folder glob.' },
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
...paginationParam,
},
},
read_lint_errors: {
name: 'read_lint_errors',
description: `Returns all lint errors on a given file.`,
params: {
...uriParam('file'),
},
},
// --- 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, ensure the path ends with a trailing slash.`,
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'),
},
@@ -145,10 +158,9 @@ export const voidTools = {
...uriParam('file'),
change_description: {
description: `\
A brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Instead, use comments like "// ... existing code ...". Bias towards writing as little as possible. \
Your description will be handed to a smaller model to make the change, so it must be clear and concise. \
Your description MUST be wrapped in triple backticks. \
A code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Bias towards writing as little as possible. \
Here's an example of a good description:\n${editToolDescriptionExample}`
}
},
@@ -158,7 +170,7 @@ Here's an example of a good description:\n${editToolDescriptionExample}`
name: 'run_terminal_command',
description: `Executes a terminal command.`,
params: {
command: { description: 'The terminal command to execute.' },
command: { description: 'The terminal command to execute. If working with tools like git that can paginate, you should pipe to cat so results are not truncated.' },
wait_for_completion: { description: `Optional. Default is true. Make this value false when you want a command to run without waiting for it to complete.` },
terminal_id: { description: 'Optional. The ID of the terminal instance that should execute the command (if not provided, defaults to the preferred terminal ID). The primary purpose of this is to let you open a new terminal for testing or background processes (e.g. running a dev server for the user in a separate terminal). Must be an integer >= 1.' },
},
@@ -205,12 +217,11 @@ Format:
}).join('\n\n')}`
}
export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
const t = toolCall
const params = Object.keys(t.rawParams).map(paramName => `<${paramName}>${t.rawParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
export const toolCallXMLStr = (toolName: ToolName, toolParams: RawToolParamsObj) => {
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
return `\
<${toolCall.name}>${!params ? '' : `\n${params}`}
</${toolCall.name}>`
<${toolName}>${!params ? '' : `\n${params}`}
</${toolName}>`
.replace('\t', ' ')
}
@@ -218,7 +229,7 @@ export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
const tools = availableTools(chatMode)
if (!tools || tools.length === 0) return ''
if (!tools || tools.length === 0) return null
const toolXMLDefinitions = (`\
Available tools:
@@ -228,8 +239,8 @@ ${availableXMLToolsStr(tools)}`)
const toolCallXMLGuidelines = (`\
Tool calling details:
- Once you write a tool call, you must STOP and WAIT for the result.
- To call a tool, write its name and parameters in one of the XML formats specified above at the BOTTOM of your response.
- All parameters are REQUIRED unless noted otherwise.
- To call a tool, write its name and parameters in one of the XML formats specified above.
- You are only allowed to output ONE tool call, and it must be at the END of your response.
- Your tool call will be executed immediately, and the results will appear in the following user message.`)
@@ -242,7 +253,7 @@ ${toolCallXMLGuidelines}`
// ======================================================== chat (normal, gather, agent) ========================================================
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => {
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode, 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.`
@@ -276,7 +287,7 @@ ${directoryStr}
</files_overview>`)
const toolDefinitions = systemToolsXMLPrompt(mode)
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
const details: string[] = []
@@ -300,8 +311,8 @@ ${directoryStr}
}
if (mode === 'gather') {
details.push(`Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.`)
details.push(`You should extensively read files, types, content, etc and gather relevant context.`)
details.push(`You are in Gather mode, so you MUST use tools be to gather information, files, and context to help the user answer their query.`)
details.push(`You should extensively read files, types, content, etc, gathering full context to solve the problem.`)
}
@@ -333,7 +344,6 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
ansStrs.push(fsInfo)
if (toolDefinitions) ansStrs.push(toolDefinitions)
ansStrs.push(importantDetails)
ansStrs.push('Now, please assist the user with their query.')
const fullSystemMsgStr = ansStrs
.join('\n\n\n')
@@ -452,13 +462,13 @@ Output SEARCH/REPLACE blocks to edit the file according to the desired change. Y
Directions:
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. This includes whitespace, comments, and other details.
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. The original code must NOT includes any new whitespace, comments, or any other modifications from the original code.
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file, but please bias towards writing as little as possible.
4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks.
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
- Make sure you add all necessary imports.
- Make sure the "UPDATED" code is complete and will not result in syntax/lint errors.
- Make sure the "UPDATED" code is ready for production as-is, and fix any relevant lint errors.
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
@@ -577,10 +587,16 @@ Instructions:
`
}
export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: {
selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string,
isOllamaFIM: false, // we require this be false for clarity
}) => {
export const ctrlKStream_userMessage = ({
selection,
prefix,
suffix,
instructions,
// isOllamaFIM: false, // Remove unused variable
fimTags,
language }: {
selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string,
}) => {
const { preTag, sufTag, midTag } = fimTags
// prompt the model artifically on how to do FIM
@@ -611,39 +627,6 @@ ${tripleTick[1]}).`
// const toAnthropicTool = (toolInfo: InternalToolInfo) => {
// const { name, description, params } = toolInfo
// return {
// name: name,
// description: description,
// input_schema: {
// type: 'object',
// properties: params,
// // required: Object.keys(params),
// },
// } satisfies Anthropic.Messages.Tool
// }
// const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
// const { name, description, params } = toolInfo
// return {
// type: 'function',
// function: {
// name: name,
// // strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat
// description: description,
// parameters: {
// type: 'object',
// properties: params,
// // required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false
// // additionalProperties: false,
// },
// }
// } satisfies OpenAI.Chat.Completions.ChatCompletionTool
// }
/*
// ======================================================== ai search/replace ========================================================
@@ -13,7 +13,6 @@ 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 { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
@@ -98,6 +97,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
return null
}
const { settingsOfProvider, } = this.voidSettingsService.state
// add state for request id
const requestId = generateUuid();
@@ -106,13 +106,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
this.llmMessageHooks.onError[requestId] = onError
this.llmMessageHooks.onAbort[requestId] = onAbort // used internally only
const { aiInstructions } = this.voidSettingsService.state.globalSettings
const { settingsOfProvider, } = this.voidSettingsService.state
// params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', {
...proxyParams,
aiInstructions,
requestId,
settingsOfProvider,
modelSelection,
@@ -27,16 +27,39 @@ export const getErrorMessage: (error: unknown) => string = (error) => {
}
export type LLMChatMessage = {
role: 'system';
content: string;
export type AnthropicLLMChatMessage = {
role: 'assistant',
content: string | (AnthropicReasoning | { type: 'text'; text: string }
| { type: 'tool_use'; name: string; input: Record<string, any>; id: string; }
)[];
} | {
role: 'user';
role: 'user',
content: string | (
{ type: 'text'; text: string; } | { type: 'tool_result'; tool_use_id: string; content: string; }
)[]
}
export type OpenAILLMChatMessage = {
role: 'system' | 'user' | 'developer';
content: string;
} | {
role: 'assistant',
content: string; // text content
anthropicReasoning: AnthropicReasoning[] | null;
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
tool_calls?: { type: 'function'; id: string; function: { name: string; arguments: string; } }[];
} | {
role: 'tool',
content: string;
tool_call_id: string;
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
export type LLMFIMMessage = {
prefix: string;
suffix: string;
stopTokens: string[];
}
@@ -47,10 +70,10 @@ export type RawToolCallObj = {
name: ToolName;
rawParams: RawToolParamsObj;
doneParams: ToolParamName[];
id: string;
isDone: boolean;
};
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj }) => void
@@ -60,23 +83,18 @@ export type OnAbort = () => void
export type AbortRef = { current: (() => void) | null }
export type LLMFIMMessage = {
prefix: string;
suffix: string;
stopTokens: string[];
}
// service types
type SendLLMType = {
messagesType: 'chatMessages';
messages: LLMChatMessage[];
messages: LLMChatMessage[]; // the type of raw chat messages that we send to Anthropic, OAI, etc
separateSystemMessage: string | undefined;
chatMode: ChatMode | null;
} | {
messagesType: 'FIMMessage';
messages: LLMFIMMessage;
separateSystemMessage?: undefined;
chatMode?: undefined;
}
// service types
export type ServiceSendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
@@ -95,8 +113,6 @@ export type SendLLMMessageParams = {
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
abortRef: AbortRef;
aiInstructions: string;
modelSelection: ModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined;
@@ -27,7 +27,8 @@ export type ToolCallParams = {
'ls_dir': { rootURI: URI, pageNumber: number },
'get_dir_structure': { rootURI: URI },
'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number },
'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
'search_for_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
'read_lint_errors': { uri: URI },
// ---
'edit_file': { uri: URI, changeDescription: string },
'create_file_or_folder': { uri: URI, isFolder: boolean },
@@ -38,11 +39,12 @@ export type ToolCallParams = {
// RESULT OF TOOL CALL
export type ToolResultType = {
'read_file': { fileContents: string, hasNextPage: boolean },
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'get_dir_structure': { str: string, },
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
'search_files': { uris: URI[], hasNextPage: boolean },
'search_for_files': { uris: URI[], hasNextPage: boolean },
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
// ---
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
'create_file_or_folder': {},
@@ -71,8 +71,8 @@ export interface IVoidSettingsService {
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[] }) => {
const { existingModels } = options
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[], didAutoDetect: boolean }) => {
const { existingModels, didAutoDetect } = options
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
for (const existingModel of existingModels) {
@@ -82,7 +82,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
const newDefaultModels = defaultModelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: true,
isAutodetected: didAutoDetect,
isHidden: !!existingModelsMap[modelName]?.isHidden,
}))
@@ -101,7 +101,30 @@ export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter:
}
const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
const _stateWithUpdatedDefaultModels = (state: VoidSettingsState): VoidSettingsState => {
let newSettingsOfProvider = state.settingsOfProvider
// recompute default models
for (const providerName of providerNames) {
const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? []
const currentModels = newSettingsOfProvider[providerName]?.models ?? []
const defaultModelNames = defaultModels.map(m => m.modelName)
const newModels = _updatedModelsAfterDefaultModelsChange(defaultModelNames, { existingModels: currentModels, didAutoDetect: false })
newSettingsOfProvider = {
...newSettingsOfProvider,
[providerName]: {
...newSettingsOfProvider[providerName],
models: newModels,
},
}
}
return {
...state,
settingsOfProvider: newSettingsOfProvider,
}
}
const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>): VoidSettingsState => {
let newSettingsOfProvider = state.settingsOfProvider
@@ -211,11 +234,21 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
async readAndInitializeState() {
const readS = await this._readState();
let readS: VoidSettingsState
try {
readS = await this._readState();
// 1.0.3 addition, remove when enough users have had this code run
if (readS.globalSettings.includeToolLintErrors === undefined) readS.globalSettings.includeToolLintErrors = true
}
catch (e) {
readS = defaultState()
}
// the stored data structure might be outdated, so we need to update it here
const finalState = readS
this.state = _validatedModelState(finalState);
this.state = readS
this.state = _stateWithUpdatedDefaultModels(this.state)
this.state = _validatedModelState(this.state);
this._resolver();
this._onDidChangeState.fire();
@@ -345,7 +378,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const { models } = this.state.settingsOfProvider[providerName]
const oldModelNames = models.map(m => m.modelName)
const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models })
const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models, didAutoDetect: true })
this.setSettingOfProvider(providerName, 'models', newModels)
// if the models changed, log it
@@ -108,6 +108,11 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
title: 'Grok (xAI)',
}
}
else if (providerName === 'mistral') {
return {
title: 'Mistral API',
}
}
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
@@ -134,7 +139,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'groq' ? 'gsk_key...' :
providerName === 'openAICompatible' ? 'sk-key...' :
providerName === 'xAI' ? 'xai-key...' :
'',
providerName === 'mistral' ? 'api-key...' :
'',
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
@@ -143,8 +149,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
providerName === 'openAICompatible' ? undefined :
'',
providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys).' :
providerName === 'openAICompatible' ? `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).` :
'',
isPasswordField: true,
}
}
@@ -234,31 +241,37 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.xAI),
_didFillInProviderSettings: undefined,
},
groq: { // aggregator
mistral: {
...defaultCustomSettings,
...defaultProviderSettings.mistral,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral),
_didFillInProviderSettings: undefined,
},
groq: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.groq,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.groq),
_didFillInProviderSettings: undefined,
},
openRouter: { // aggregator
openRouter: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openRouter),
_didFillInProviderSettings: undefined,
},
openAICompatible: { // aggregator
openAICompatible: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAICompatible),
_didFillInProviderSettings: undefined,
},
ollama: { // aggregator
ollama: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.ollama),
_didFillInProviderSettings: undefined,
},
vLLM: { // aggregator
vLLM: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.vLLM,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
@@ -356,6 +369,8 @@ export type GlobalSettings = {
chatMode: ChatMode;
autoApprove: boolean;
showInlineSuggestions: boolean;
includeToolLintErrors: boolean;
isOnboardingComplete: boolean;
}
export const defaultGlobalSettings: GlobalSettings = {
@@ -367,6 +382,8 @@ export const defaultGlobalSettings: GlobalSettings = {
chatMode: 'agent',
autoApprove: false,
showInlineSuggestions: true,
includeToolLintErrors: true,
isOnboardingComplete: false,
}
export type GlobalSettingName = keyof GlobalSettings
@@ -7,12 +7,13 @@ import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
import { VoidCheckUpdateRespose } from './voidUpdateServiceTypes.js';
export interface IVoidUpdateService {
readonly _serviceBrand: undefined;
check: (explicit: boolean) => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>;
check: (explicit: boolean) => Promise<VoidCheckUpdateRespose>;
}
@@ -0,0 +1,14 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
export type VoidCheckUpdateRespose = {
message: string,
action?: 'reinstall' | 'restart' | 'download' | 'apply'
} | {
message: null,
actions?: undefined,
} | null
@@ -3,11 +3,11 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { endsWithAnyPrefixOf } from '../../common/helpers/extractCodeFromResult.js'
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 { OnFinalMessage, OnText, RawToolCallObj } from '../../common/sendLLMMessageTypes.js'
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
import { ChatMode } from '../../common/voidSettingsTypes.js'
import { createSaxParser } from './sax.js'
// =============== reasoning ===============
@@ -135,146 +135,148 @@ export const extractReasoningWrapper = (
}
// =============== tools ===============
// =============== tools (XML) ===============
type ToolsState = {
level: 'normal',
} | {
level: 'tool',
toolName: ToolName,
currentToolCall: RawToolCallObj,
} | {
level: 'param',
toolName: ToolName,
paramName: ToolParamName,
currentToolCall: RawToolCallObj,
const findPartiallyWrittenToolTagAtEnd = (fullText: string, toolTags: string[]) => {
for (const toolTag of toolTags) {
const foundPrefix = endsWithAnyPrefixOf(fullText, toolTag)
if (foundPrefix) {
return [foundPrefix, toolTag] as const
}
}
return false
}
export const extractToolsWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode
const findIndexOfAny = (fullText: string, matches: string[]) => {
for (const str of matches) {
const idx = fullText.indexOf(str);
if (idx !== -1) {
return [idx, str] as const
}
}
return null
}
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
const paramsObj: RawToolParamsObj = {}
const doneParams: ToolParamName[] = []
let isDone = false
const getAnswer = (): RawToolCallObj => {
// trim off all whitespace at and before first \n and after last \n for each param
for (const p in paramsObj) {
const paramName = p as ToolParamName
const orig = paramsObj[paramName]
if (orig === undefined) continue
paramsObj[paramName] = trimBeforeAndAfterNewLines(orig)
}
// return tool call
const ans: RawToolCallObj = {
name: toolName,
rawParams: paramsObj,
doneParams: doneParams,
isDone: isDone,
id: toolId,
}
return ans
}
// find first toolName tag
const openToolTag = `<${toolName}>`
let i = str.indexOf(openToolTag)
if (i === -1) return getAnswer()
let j = str.lastIndexOf(`</${toolName}>`)
if (j === -1) j = Infinity
else isDone = true
str = str.substring(i + openToolTag.length, j)
const pm = new SurroundingsRemover(str)
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[]
if (allowedParams.length === 0) return getAnswer()
let latestMatchedOpenParam: null | ToolParamName = null
let n = 0
while (true) {
n += 1
if (n > 10) return getAnswer() // just for good measure as this code is early
// find the param name opening tag
let matchedOpenParam: null | ToolParamName = null
for (const paramName of allowedParams) {
const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true)
if (removed) {
matchedOpenParam = paramName
break
}
}
// if did not find a new param, stop
if (matchedOpenParam === null) {
if (latestMatchedOpenParam !== null) {
paramsObj[latestMatchedOpenParam] += pm.value()
}
return getAnswer()
}
else {
latestMatchedOpenParam = matchedOpenParam
}
paramsObj[latestMatchedOpenParam] = ''
// find the param name closing tag
let matchedCloseParam: boolean = false
let paramContents = ''
for (const paramName of allowedParams) {
const i = pm.i
const closeTag = `</${paramName}>`
const removed = pm.removeFromStartUntilFullMatch(closeTag, true)
if (removed) {
const i2 = pm.i
paramContents = pm.originalS.substring(i, i2 - closeTag.length)
matchedCloseParam = true
break
}
}
// if did not find a new close tag, stop
if (!matchedCloseParam) {
paramsObj[latestMatchedOpenParam] += pm.value()
return getAnswer()
}
else {
doneParams.push(latestMatchedOpenParam)
}
paramsObj[latestMatchedOpenParam] += paramContents
}
}
export const extractXMLToolsWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
const tools = availableTools(chatMode)
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
const toolOfToolName: { [toolName: string]: InternalToolInfo | undefined } = {}
const toolOfToolName: ToolOfToolName = {}
const toolOpenTags = tools.map(t => `<${t.name}>`)
for (const t of tools) { toolOfToolName[t.name] = t }
const toolId = generateUuid()
// detect <availableTools[0]></availableTools[0]>, etc
let fullText = '';
let trueFullText = ''
const firstToolCallRef: { current: RawToolCallObj | undefined } = { current: undefined }
let state: ToolsState = { level: 'normal' }
const getRawNewText = () => {
return trueFullText.substring(parser.startTagPosition, parser.position + 1)
}
const parser = createSaxParser()
// when see open tag <tagName>
parser.onopentag = (node) => {
const rawNewText = getRawNewText()
const tagName = node.name;
console.log('OPENING', tagName)
console.log('state0:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
if (state.level === 'normal') {
if (tagName in toolOfToolName) { // valid toolName
state = {
level: 'tool',
toolName: tagName as ToolName,
currentToolCall: { name: tagName as ToolName, rawParams: {}, doneParams: [], isDone: false }
}
firstToolCallRef.current = state.currentToolCall
}
else {
fullText += rawNewText // count as plaintext
console.log('adding raw a', rawNewText)
}
}
else if (state.level === 'tool') {
if (tagName in (toolOfToolName[state.toolName]?.params ?? {})) { // valid param
state = {
level: 'param',
toolName: state.toolName,
paramName: tagName as ToolParamName,
currentToolCall: state.currentToolCall,
}
}
else {
// would normally be rawNewText, but we ignore all text inside tools
}
}
else if (state.level === 'param') { // cannot double nest
fullText += rawNewText // count as plaintext
console.log('adding raw b', rawNewText)
}
console.log('state1:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
};
parser.onclosetag = (tagName) => {
const rawNewText = getRawNewText()
console.log('CLOSING', tagName)
console.log('state0:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
if (state.level === 'normal') {
fullText += rawNewText
console.log('adding raw A', rawNewText)
}
else if (state.level === 'tool') {
if (tagName === state.toolName) { // closed the tool
state.currentToolCall.isDone = true
state = {
level: 'normal',
}
}
else { // add as text
fullText += rawNewText
console.log('adding raw B', rawNewText)
}
}
else if (state.level === 'param') {
if (tagName === state.paramName) { // closed the param
state.currentToolCall.doneParams.push(state.paramName)
state = {
level: 'tool',
toolName: state.toolName,
currentToolCall: state.currentToolCall,
}
}
else {
fullText += rawNewText
console.log('adding raw C', rawNewText)
}
}
console.log('state1:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
};
parser.ontext = (text) => {
if (state.level === 'normal') {
fullText += text
}
// start param
else if (state.level === 'tool') {
// ignore all text in a tool, all text should go in the param tags inside it
}
else if (state.level === 'param') {
if (!(state.paramName in state.currentToolCall.rawParams)) state.currentToolCall.rawParams[state.paramName] = ''
state.currentToolCall.rawParams[state.paramName] += text
}
}
let latestToolCall: RawToolCallObj | undefined = undefined
let foundOpenTag: { idx: number, toolName: ToolName } | null = null
let openToolTagBuffer = '' // the characters we've seen so far that come after a < with no space afterwards, not yet added to fullText
let prevFullTextLen = 0
const newOnText: OnText = (params) => {
@@ -282,13 +284,53 @@ export const extractToolsWrapper = (
prevFullTextLen = params.fullText.length
trueFullText = params.fullText
parser.write(newText)
// console.log('NEWTEXT', JSON.stringify(newText))
if (foundOpenTag === null) {
const newFullText = openToolTagBuffer + newText
// ensure the code below doesn't run if only half a tag has been written
const isPartial = findPartiallyWrittenToolTagAtEnd(newFullText, toolOpenTags)
if (isPartial) {
// console.log('--- partial!!!')
openToolTagBuffer += newText
}
// if no tooltag is partially written at the end, attempt to get the index
else {
// we will instantly retroactively remove this if it's a tag match
fullText += openToolTagBuffer
openToolTagBuffer = ''
fullText += newText
const i = findIndexOfAny(fullText, toolOpenTags)
if (i !== null) {
const [idx, toolTag] = i
const toolName = toolTag.substring(1, toolTag.length - 1) as ToolName
// console.log('found ', toolName)
foundOpenTag = { idx, toolName }
// do not count anything at or after i in fullText
fullText = fullText.substring(0, idx)
}
}
}
// toolTagIdx is not null, so parse the XML
if (foundOpenTag !== null) {
latestToolCall = parseXMLPrefixToToolCall(
foundOpenTag.toolName,
toolId,
trueFullText.substring(foundOpenTag.idx, Infinity),
toolOfToolName,
)
}
// firstToolCallRef.current === state.currentToolCall is always true
onText({
...params,
fullText,
toolCall: firstToolCallRef.current,
toolCall: latestToolCall,
});
};
@@ -298,16 +340,7 @@ export const extractToolsWrapper = (
newOnText({ ...params })
fullText = fullText.trimEnd()
const toolCall = firstToolCallRef.current
if (toolCall) {
// trim off all whitespace at and before first \n and after last \n for each param
for (const p in toolCall.rawParams) {
const paramName = p as ToolParamName
const orig = toolCall.rawParams[paramName]
if (orig === undefined) continue
toolCall.rawParams[paramName] = trimBeforeAndAfterNewLines(orig)
}
}
const toolCall = latestToolCall
// console.log('final message!!!', trueFullText)
// console.log('----- returning ----\n', fullText)
@@ -321,7 +354,7 @@ export const extractToolsWrapper = (
// trim all whitespace up until the first newline, and all whitespace after the last newline
// trim all whitespace up until the first newline, and all whitespace up until the last newline
const trimBeforeAndAfterNewLines = (s: string) => {
if (!s) return s;
@@ -1,524 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { AnthropicReasoning, LLMChatMessage, LLMFIMMessage } from '../../common/sendLLMMessageTypes.js';
import { deepClone } from '../../../../../base/common/objects.js';
export const parseObject = (args: unknown) => {
if (typeof args === 'object')
return args
if (typeof args === 'string')
try { return JSON.parse(args) }
catch (e) { return { args } }
return {}
}
type InternalLLMChatMessage = {
role: 'system' | 'user';
content: string;
} | {
role: 'assistant',
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
}
const EMPTY_MESSAGE = '(empty message)'
const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }): { messages: LLMChatMessage[] } => {
const messages = deepClone(messages_)
const newMessages: LLMChatMessage[] = []
if (messages.length >= 0) newMessages.push(messages[0])
// remove duplicate roles - we used to do this, but we don't anymore
for (let i = 1; i < messages.length; i += 1) {
const m = messages[i]
newMessages.push(m)
}
const finalMessages = newMessages.map(m => ({ ...m, content: m.content.trim() }))
return { messages: finalMessages }
}
const CHARS_PER_TOKEN = 4
const TRIM_TO_LEN = 60
const prepareMessages_fitIntoContext = ({ messages, contextWindow, maxOutputTokens }: { messages: LLMChatMessage[], contextWindow: number, maxOutputTokens: number }): { messages: LLMChatMessage[] } => {
// the higher the weight, the higher the desire to truncate
const alreadyTrimmedIdxes = new Set<number>()
const weight = (message: LLMChatMessage, messages: LLMChatMessage[], idx: number) => {
const base = message.content.length
let multiplier: number
if (message.role === 'system')
return 0 // never erase system message
multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases
if (message.role === 'user') {
multiplier *= 1
}
else {
multiplier *= 10 // llm tokens are far less valuable than user tokens
}
// 1st message, last 3 msgs, any already modified message should be low in weight
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
multiplier *= .05
}
return base * multiplier
}
const _findLargestByWeight = (messages: LLMChatMessage[]) => {
let largestIndex = -1
let largestWeight = -Infinity
for (let i = 0; i < messages.length; i += 1) {
const m = messages[i]
const w = weight(m, messages, i)
if (w > largestWeight) {
largestWeight = w
largestIndex = i
}
}
return largestIndex
}
let totalLen = 0
for (const m of messages) { totalLen += m.content.length }
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
if (charsNeedToTrim <= 0) return { messages }
// <----------------------------------------->
// 0 | | |
// | contextWindow |
// contextWindow - maxOut|putTokens
// |
// totalLen
// TRIM HIGHEST WEIGHT MESSAGES
let remainingCharsToTrim = charsNeedToTrim
let i = 0
while (remainingCharsToTrim > 0) {
i += 1
if (i > 100) break
const trimIdx = _findLargestByWeight(messages)
const m = messages[trimIdx]
// if can finish here, do
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
if (numCharsWillTrim > remainingCharsToTrim) {
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim)
break
}
remainingCharsToTrim -= numCharsWillTrim
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
alreadyTrimmedIdxes.add(trimIdx)
}
return { messages }
}
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
const prepareMessages_addSystemInstructions = ({
messages,
aiInstructions,
supportsSystemMessage,
}: {
messages: InternalLLMChatMessage[],
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
})
: { separateSystemMessageStr?: string, messages: any[] } => {
// find system messages and concatenate them
let systemMessageStr = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n') || undefined;
if (aiInstructions)
systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}`
let separateSystemMessageStr: string | undefined = undefined
// remove all system messages
const newMessages: (InternalLLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system')
// if it has a system message (if doesn't, we obviously don't care about whether it supports system message or not...)
if (systemMessageStr) {
// if supports system message
if (supportsSystemMessage) {
if (supportsSystemMessage === 'separated')
separateSystemMessageStr = systemMessageStr
else if (supportsSystemMessage === 'system-role')
newMessages.unshift({ role: 'system', content: systemMessageStr }) // add new first message
else if (supportsSystemMessage === 'developer-role')
newMessages.unshift({ role: 'developer', content: systemMessageStr }) // add new first message
}
// if does not support system message
else {
const newFirstMessage = {
role: 'user',
content: (''
+ '<SYSTEM_MESSAGE>\n'
+ systemMessageStr
+ '\n'
+ '</SYSTEM_MESSAGE>\n'
+ newMessages[0].content
)
} as const
newMessages.splice(0, 1) // delete first message
newMessages.unshift(newFirstMessage) // add new first message
}
}
return { messages: newMessages, separateSystemMessageStr }
}
// // convert messages as if about to send to openai
// /*
// reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
// openai MESSAGE (role=assistant):
// "tool_calls":[{
// "type": "function",
// "id": "call_12345xyz",
// "function": {
// "name": "get_weather",
// "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
// }]
// openai RESPONSE (role=user):
// { "role": "tool",
// "tool_call_id": tool_call.id,
// "content": str(result) }
// also see
// openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
// openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
// */
// type PrepareMessagesToolsOpenAI = (
// Exclude<InternalLLMChatMessage, { role: 'assistant' | 'tool' }> | {
// role: 'assistant',
// content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
// tool_calls?: {
// type: 'function';
// id: string;
// function: {
// name: string;
// arguments: string;
// }
// }[]
// } | {
// role: 'tool',
// tool_call_id: string;
// content: string;
// }
// )[]
// const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
// const newMessages: PrepareMessagesToolsOpenAI = [];
// for (let i = 0; i < messages.length; i += 1) {
// const currMsg = messages[i]
// if (currMsg.role !== 'tool') {
// newMessages.push(currMsg)
// continue
// }
// // edit previous assistant message to have called the tool
// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
// if (prevMsg?.role === 'assistant') {
// prevMsg.tool_calls = [{
// type: 'function',
// id: currMsg.id,
// function: {
// name: currMsg.name,
// arguments: JSON.stringify(currMsg.params)
// }
// }]
// }
// // add the tool
// newMessages.push({
// role: 'tool',
// tool_call_id: currMsg.id,
// content: currMsg.content || EMPTY_TOOL_CONTENT,
// })
// }
// return { messages: newMessages }
// }
// // convert messages as if about to send to anthropic
// /*
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
// anthropic MESSAGE (role=assistant):
// "content": [{
// "type": "text",
// "text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
// }, {
// "type": "tool_use",
// "id": "toolu_01A09q90qw90lq917835lq9",
// "name": "get_weather",
// "input": { "location": "San Francisco, CA", "unit": "celsius" }
// }]
// anthropic RESPONSE (role=user):
// "content": [{
// "type": "tool_result",
// "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
// "content": "15 degrees"
// }]
// */
// type PrepareMessagesToolsAnthropic = (
// Exclude<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
// role: 'assistant',
// content: string | (
// | AnthropicReasoning
// | {
// type: 'text';
// text: string;
// }
// | {
// type: 'tool_use';
// name: string;
// input: Record<string, any>;
// id: string;
// })[]
// } | {
// role: 'user',
// content: string | ({
// type: 'text';
// text: string;
// } | {
// type: 'tool_result';
// tool_use_id: string;
// content: string;
// })[]
// }
// )[]
// /*
// Converts:
// assistant: ...content
// tool: (id, name, params)
// ->
// assistant: ...content, call(name, id, params)
// user: ...content, result(id, content)
// */
// const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
// const newMessages: PrepareMessagesToolsAnthropic = messages;
// for (let i = 0; i < newMessages.length; i += 1) {
// const currMsg = newMessages[i]
// if (currMsg.role !== 'tool') continue
// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
// if (prevMsg?.role === 'assistant') {
// if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
// prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
// }
// // turn each tool into a user message with tool results at the end
// newMessages[i] = {
// role: 'user',
// content: [
// ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const,
// ]
// }
// }
// return { messages: newMessages }
// }
// type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI
// const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style' }): { messages: PrepareMessagesTools } => {
// if (!supportsTools) {
// return { messages: messages }
// }
// else if (supportsTools === 'anthropic-style') {
// return prepareMessages_tools_anthropic({ messages })
// }
// else if (supportsTools === 'openai-style') {
// return prepareMessages_tools_openai({ messages })
// }
// else {
// throw new Error(`supportsTools type not recognized`)
// }
// }
// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent
const prepareMessages_anthropicReasoning = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => {
const newMessages: InternalLLMChatMessage[] = []
for (const m of messages) {
if (m.role !== 'assistant') {
newMessages.push(m)
continue
}
let newMessage: InternalLLMChatMessage
if (supportsAnthropicReasoningSignature && m.anthropicReasoning) {
const content = m.content ? [...m.anthropicReasoning, { type: 'text' as const, text: m.content }] : m.anthropicReasoning
newMessage = { role: 'assistant', content: content }
}
else {
newMessage = { role: 'assistant', content: m.content }
}
newMessages.push(newMessage)
}
return { messages: newMessages }
}
// do this at end
const prepareMessages_noEmptyMessage = ({ messages }: { messages: InternalLLMChatMessage[] }): { messages: InternalLLMChatMessage[] } => {
for (const currMsg of messages) {
// if content is a string, replace string with empty msg
if (typeof currMsg.content === 'string')
currMsg.content = currMsg.content || EMPTY_MESSAGE
else {
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
for (const c of currMsg.content) {
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
}
if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
}
}
return { messages }
}
// --- CHAT ---
export const prepareMessages = ({
messages,
aiInstructions,
supportsSystemMessage,
supportsAnthropicReasoningSignature,
contextWindow,
maxOutputTokens,
}: {
messages: LLMChatMessage[],
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
supportsAnthropicReasoningSignature: boolean,
contextWindow: number,
maxOutputTokens: number | null | undefined,
}) => {
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
const { messages: messages0 } = prepareMessages_normalize({ messages })
const { messages: messages1 } = prepareMessages_fitIntoContext({ messages: messages0, contextWindow, maxOutputTokens })
const { messages: messages2 } = prepareMessages_anthropicReasoning({ messages: messages1, supportsAnthropicReasoningSignature })
const { messages: messages3, separateSystemMessageStr } = prepareMessages_addSystemInstructions({ messages: messages2, aiInstructions, supportsSystemMessage })
const { messages: messages4 } = prepareMessages_noEmptyMessage({ messages: messages3 })
return {
messages: messages4 as any,
separateSystemMessageStr
} as const
}
// --- FIM ---
export const prepareFIMMessage = ({
messages,
aiInstructions,
}: {
messages: LLMFIMMessage,
aiInstructions: string,
}) => {
let prefix = `\
${!aiInstructions ? '' : `\
// Instructions:
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
${messages.prefix}`
const suffix = messages.suffix
const stopTokens = messages.stopTokens
const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const
return ret
}
/*
Gemini has this, but they're openai-compat so we don't need to implement this
gemini request:
{ "role": "assistant",
"content": null,
"function_call": {
"name": "get_weather",
"arguments": {
"latitude": 48.8566,
"longitude": 2.3522
}
}
}
gemini response:
{ "role": "assistant",
"function_response": {
"name": "get_weather",
"response": {
"temperature": "15°C",
"condition": "Cloudy"
}
}
}
*/
@@ -1,150 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// Define options for the parser.
export interface SaxParserOptions {
lowercase?: boolean;
}
// Define the structure for a parsed node.
export interface SaxNode {
name: string;
attributes: { [key: string]: string };
}
// Define the interface for the SAX-like parser.
export interface SaxParser {
// Event handlers that can be set by the consumer.
onopentag: ((node: SaxNode) => void) | null;
ontext: ((text: string) => void) | null;
onclosetag: ((tagName: string) => void) | null;
// Properties to track current positions (used for raw text extraction).
startTagPosition: number;
position: number;
// Processes a new chunk of text.
write(chunk: string): void;
}
/**
* Creates a minimal, event-driven SAX-like parser.
*
* @param options An object of type `SaxParserOptions`. Passing `{ lowercase: true }` will force all tag names to be lower-cased.
* @returns A parser object implementing the `SaxParser` interface.
*/
export function createSaxParser(options: SaxParserOptions = {}): SaxParser {
// Buffer to hold any leftover text (part of an incomplete tag).
let buffer: string = '';
// Global counter to track the total processed characters.
let globalPos: number = 0;
const parser: SaxParser = {
onopentag: null,
ontext: null,
onclosetag: null,
startTagPosition: 0,
position: 0,
write(chunk: string): void {
// Set the starting position before processing the new chunk.
this.startTagPosition = globalPos;
buffer += chunk;
globalPos += chunk.length;
// Set the current position to the end of the processed chunk.
this.position = globalPos - 1;
let cursor = 0;
// Flag to indicate if an incomplete tag was found.
let incompleteTagFound = false;
// This will mark the position in the buffer where the incomplete tag starts.
let incompleteStart = 0;
while (cursor < buffer.length) {
// Look for the next opening '<' character.
const ltIndex = buffer.indexOf('<', cursor);
if (ltIndex === -1) {
// No more tags found in the current buffer.
if (cursor < buffer.length && this.ontext) {
this.ontext(buffer.substring(cursor));
}
// All content is processed.
buffer = '';
cursor = buffer.length;
break;
}
// Emit any text between the current cursor and the opening tag.
if (ltIndex > cursor && this.ontext) {
this.ontext(buffer.substring(cursor, ltIndex));
}
// Look for the closing '>' character starting from the found '<'.
const gtIndex = buffer.indexOf('>', ltIndex);
if (gtIndex === -1) {
// Incomplete tag detected.
incompleteTagFound = true;
// Save the starting point of the incomplete tag.
incompleteStart = ltIndex;
break;
}
// Extract the tag content (excluding the '<' and '>').
let tagContent = buffer.substring(ltIndex + 1, gtIndex).trim();
if (!tagContent) {
cursor = gtIndex + 1;
continue;
}
// Check if this is a closing tag (starts with '/').
if (tagContent[0] === '/') {
let tagName = tagContent.substring(1).trim();
if (options.lowercase && tagName) {
tagName = tagName.toLowerCase();
}
if (this.onclosetag) {
this.onclosetag(tagName);
}
} else {
// Handle self-closing tags (ending with '/').
let selfClosing = false;
if (tagContent[tagContent.length - 1] === '/') {
selfClosing = true;
tagContent = tagContent.slice(0, -1).trim();
}
// Determine the tag name (first word before any whitespace).
const spaceIndex = tagContent.indexOf(' ');
let tagName =
spaceIndex !== -1
? tagContent.substring(0, spaceIndex).trim()
: tagContent;
if (options.lowercase && tagName) {
tagName = tagName.toLowerCase();
}
// Emit an open tag event.
if (this.onopentag) {
const node: SaxNode = { name: tagName, attributes: {} };
this.onopentag(node);
}
// If its a self-closing tag, immediately emit a close tag event.
if (selfClosing && this.onclosetag) {
this.onclosetag(tagName);
}
}
// Move the cursor past the current tag.
cursor = gtIndex + 1;
}
// If an incomplete tag was detected, preserve it.
if (incompleteTagFound) {
// Keep the incomplete portion starting from the '<'
buffer = buffer.substring(incompleteStart);
} else {
// Otherwise, remove all processed content.
buffer = buffer.substring(cursor);
}
},
};
return parser;
}
@@ -6,16 +6,18 @@
import Anthropic from '@anthropic-ai/sdk';
import { Ollama } from 'ollama';
import OpenAI, { ClientOptions } from 'openai';
import { MistralCore } from '@mistralai/mistralai/core.js';
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js';
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings } from '../../common/modelCapabilities.js';
import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js';
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
type InternalCommonMessageParams = {
aiInstructions: string;
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
@@ -26,8 +28,8 @@ type InternalCommonMessageParams = {
_setAborter: (aborter: () => void) => void;
}
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; }
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; }
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
@@ -84,12 +86,16 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'mistral') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else throw new Error(`Void providerName was invalid: ${providerName}.`)
}
const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => {
const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
if (!supportsFIM) {
if (modelName === modelName_)
@@ -99,16 +105,14 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
return
}
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
openai.completions
.create({
model: modelName,
prompt: messages.prefix,
suffix: messages.suffix,
stop: messages.stopTokens,
max_tokens: messages.maxTokens,
prompt: prefix,
suffix: suffix,
stop: stopTokens,
max_tokens: 300,
})
.then(async response => {
const fullText = response.choices[0]?.text
@@ -121,14 +125,61 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
}
const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
const { name, description, params } = toolInfo
return {
type: 'function',
function: {
name: name,
// strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat
description: description,
parameters: {
type: 'object',
properties: params,
// required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false
// additionalProperties: false,
},
}
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
}
const openAITools = (chatMode: ChatMode) => {
const allowedTools = availableTools(chatMode)
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
for (const t in allowedTools ?? {}) {
openAITools.push(toOpenAICompatibleTool(allowedTools[t]))
}
return openAITools
}
const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
if (!isAToolName(name)) return null
const rawParams: RawToolParamsObj = {}
let input: unknown
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 _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, chatMode }: SendChatParams_Internal) => {
// ------------ OPENAI-COMPATIBLE ------------
const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
const {
modelName,
supportsSystemMessage,
contextWindow,
maxOutputTokens,
specialToolFormat,
reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_)
@@ -139,16 +190,17 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// max tokens
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
// tools
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
const nativeToolsObj = potentialTools ? { tools: potentialTools } as const : {}
// instance
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens })
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelName,
messages: messages,
messages: messages as any,
stream: true,
...nativeToolsObj,
// max_completion_tokens: maxTokens,
}
@@ -161,9 +213,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
onFinalMessage = newOnFinalMessage
}
// manually parse out tool results
if (chatMode) {
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
// manually parse out tool results if XML
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
@@ -171,6 +223,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
let fullReasoningSoFar = ''
let fullTextSoFar = ''
let toolName = ''
let toolId = ''
let toolParamsStr = ''
openai.chat.completions
.create(options)
.then(async response => {
@@ -181,6 +237,17 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const newText = chunk.choices[0]?.delta?.content ?? ''
fullTextSoFar += newText
// tool call
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
const index = tool.index
if (index !== 0) continue
toolName += tool.function?.name ?? ''
toolParamsStr += tool.function?.arguments ?? '';
toolId += tool.id ?? ''
}
// reasoning
let newReasoning = ''
if (nameOfReasoningFieldInDelta) {
@@ -189,14 +256,21 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
fullReasoningSoFar += newReasoning
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
})
}
// on final
if (!fullTextSoFar && !fullReasoningSoFar) {
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
onError({ message: 'Void: Response from model was empty.', fullError: null })
}
else {
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null });
const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId)
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
}
})
// when error/fail - this catches errors of both .create() and .then(for await)
@@ -244,14 +318,48 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
// ------------ ANTHROPIC (HELPERS) ------------
const toAnthropicTool = (toolInfo: InternalToolInfo) => {
const { name, description, params } = toolInfo
return {
name: name,
description: description,
input_schema: {
type: 'object',
properties: params,
// required: Object.keys(params),
},
} satisfies Anthropic.Messages.Tool
}
const anthropicTools = (chatMode: ChatMode) => {
const allowedTools = availableTools(chatMode)
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
for (const t in allowedTools ?? {}) {
anthropicTools.push(toAnthropicTool(allowedTools[t]))
}
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 = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, chatMode }: SendChatParams_Internal) => {
const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const {
modelName,
supportsSystemMessage,
contextWindow,
maxOutputTokens,
reasoningCapabilities,
specialToolFormat,
} = getModelCapabilities(providerName, modelName_)
const thisConfig = settingsOfProvider.anthropic
@@ -262,26 +370,32 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// anthropic-specific - max tokens
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled })
// tools
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
const nativeToolsObj = potentialTools ? { tools: potentialTools, tool_choice: { type: 'auto' } } as const : {}
// instance
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
const anthropic = new Anthropic({
apiKey: thisConfig.apiKey,
dangerouslyAllowBrowser: true
});
const stream = anthropic.messages.stream({
system: separateSystemMessageStr,
messages: messages,
system: separateSystemMessage ?? undefined,
messages: messages as AnthropicLLMChatMessage[],
model: modelName,
max_tokens: maxTokens ?? 4_096, // anthropic requires this
...includeInPayload,
...nativeToolsObj,
})
// manually parse out tool results
if (chatMode) {
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
@@ -290,9 +404,17 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
let fullText = ''
let fullReasoning = ''
// let fullToolName = ''
// let fullToolParams = ''
let fullToolName = ''
let fullToolParams = ''
const runOnText = () => {
onText({
fullText,
fullReasoning,
toolCall: isAToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
})
}
// there are no events for tool_use, it comes in at the end
stream.on('streamEvent', e => {
// start block
@@ -300,46 +422,49 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
if (e.content_block.type === 'text') {
if (fullText) fullText += '\n\n' // starting a 2nd text block
fullText += e.content_block.text
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.content_block.type === 'thinking') {
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
fullReasoning += e.content_block.thinking
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.content_block.type === 'redacted_thinking') {
console.log('delta', e.content_block.type)
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
fullReasoning += '[redacted_thinking]'
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.content_block.type === 'tool_use') {
fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
runOnText()
}
// else if (e.content_block.type === 'tool_use') {
// fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
// onText({ fullText, fullReasoning, })
// }
}
// delta
else if (e.type === 'content_block_delta') {
if (e.delta.type === 'text_delta') {
fullText += e.delta.text
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.delta.type === 'thinking_delta') {
fullReasoning += e.delta.thinking
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.delta.type === 'input_json_delta') { // tool use
fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
runOnText()
}
// else if (e.delta.type === 'input_json_delta') { // tool use
// fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
// onText({ fullText, fullReasoning, })
// }
}
})
// on done - (or when error/fail) - this is called AFTER last streamEvent
stream.on('finalMessage', (response) => {
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
onFinalMessage({ fullText, fullReasoning, anthropicReasoning })
const tools = response.content.filter(c => c.type === 'tool_use')
const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0])
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
})
// on error
stream.on('error', (error) => {
@@ -349,6 +474,43 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
_setAborter(() => stream.controller.abort())
}
// ------------ MISTRAL ------------
// https://docs.mistral.ai/api/#tag/fim
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
if (!supportsFIM) {
if (modelName === modelName_)
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
else
onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null })
return
}
const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey })
fimComplete(mistral,
{
model: modelName,
prompt: messages.prefix,
suffix: messages.suffix,
stream: false,
maxTokens: 300,
stop: messages.stopTokens,
})
.then(async response => {
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
const fullText = typeof content === 'string' ? content
: content.map(chunk => (chunk.type === 'text' ? chunk.text : '')).join('')
onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null });
})
.catch(error => {
onError({ message: error + '', fullError: error });
})
}
// ------------ OLLAMA ------------
const newOllamaSDK = ({ endpoint }: { endpoint: string }) => {
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
@@ -381,12 +543,10 @@ const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOf
}
}
const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName, aiInstructions, _setAborter }: SendFIMParams_Internal) => {
const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => {
const thisConfig = settingsOfProvider.ollama
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
let fullText = ''
ollama.generate({
model: modelName,
@@ -394,7 +554,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO
suffix: messages.suffix,
options: {
stop: messages.stopTokens,
num_predict: messages.maxTokens, // max tokens
num_predict: 300, // max tokens
// repeat_penalty: 1,
},
raw: true,
@@ -445,11 +605,11 @@ export const sendLLMMessageToProviderImplementation = {
sendFIM: null,
list: null,
},
// mistral: {
// sendChat: , // TODO
// sendFIM: , // TODO // https://docs.mistral.ai/api/#tag/fim
// list: null,
// },
mistral: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: (params) => sendMistralFIM(params),
list: null,
},
ollama: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: sendOllamaFIM,
@@ -11,7 +11,6 @@ import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js
export const sendLLMMessage = ({
messagesType,
aiInstructions,
messages: messages_,
onText: onText_,
onFinalMessage: onFinalMessage_,
@@ -22,6 +21,7 @@ export const sendLLMMessage = ({
modelSelection,
modelSelectionOptions,
chatMode,
separateSystemMessage,
}: SendLLMMessageParams,
metricsService: IMetricsService
@@ -108,12 +108,12 @@ export const sendLLMMessage = ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, chatMode })
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
return
}
if (messagesType === 'FIMMessage') {
if (sendFIM) {
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions })
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
return
}
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
@@ -8,6 +8,7 @@ import { IEnvironmentMainService } from '../../../../platform/environment/electr
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IUpdateService, StateType } from '../../../../platform/update/common/update.js';
import { IVoidUpdateService } from '../common/voidUpdateService.js';
import { VoidCheckUpdateRespose } from '../common/voidUpdateServiceTypes.js';
@@ -17,48 +18,86 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
constructor(
@IProductService private readonly _productService: IProductService,
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
@IUpdateService private readonly _updateService: IUpdateService
@IUpdateService private readonly _updateService: IUpdateService,
) {
super()
}
nIgnores = 0
async check(explicit: boolean) {
async check(explicit: boolean): Promise<VoidCheckUpdateRespose> {
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
if (isDevMode) {
return { hasUpdate: false } as const
return { message: null } as const
}
this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves
console.log('updateState', this._updateService.state)
if (this._updateService.state.type === StateType.Uninitialized) {
// The update service hasn't been initialized yet
return { message: explicit ? 'Not yet checking for updates...' : null, action: explicit ? 'reinstall' : undefined } as const
}
if (this._updateService.state.type === StateType.Idle) {
// No updates currently available
return { message: explicit ? 'No update found!' : null, action: explicit ? 'reinstall' : undefined } as const
}
if (this._updateService.state.type === StateType.CheckingForUpdates) {
// Currently checking for updates
return { message: explicit ? 'No updates found!' : null } as const
}
if (this._updateService.state.type === StateType.AvailableForDownload) {
// Update available but requires manual download (mainly for Linux)
return { message: 'A new update is available!', action: 'download', } as const
}
if (this._updateService.state.type === StateType.Downloading) {
// Update is currently being downloaded
return { message: explicit ? 'Currently downloading update...' : null } as const
}
if (this._updateService.state.type === StateType.Downloaded) {
// Update has been downloaded but not yet ready
return { message: explicit ? 'Got download, need to apply...' : null, action: 'apply' } as const
}
if (this._updateService.state.type === StateType.Updating) {
// Update is being applied
return { message: explicit ? 'Applying update...' : null } as const
}
if (this._updateService.state.type === StateType.Ready) {
return { hasUpdate: true, message: 'Restart Void to update!' }
// Update is ready
return { message: 'Restart Void to update!', action: 'restart' } as const
}
const wasAutomaticCheck = !explicit // ignore the first auto check, just use it to call updateService.check()
if (wasAutomaticCheck && this.nIgnores < 1) {
this.nIgnores += 1
return { hasUpdate: false } as const
}
if (this._updateService.state.type === StateType.Disabled) {
try {
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
const resJSON = await res.json()
try {
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
const resJSON = await res.json()
if (!resJSON) return null // null means error
if (!resJSON) return null // null means error
const { hasUpdate, downloadMessage } = resJSON ?? {}
if (hasUpdate === undefined)
return null
const { hasUpdate, downloadMessage } = resJSON ?? {}
if (hasUpdate === undefined)
const after = (downloadMessage || '') + ''
if (hasUpdate)
return { message: after, action: 'reinstall' } as const
return { message: 'Void is up-to-date!' } as const
}
catch (e) {
return null
}
}
return null
const after = (downloadMessage || '') + ''
return { hasUpdate: !!hasUpdate, message: after }
}
catch (e) {
return null
}
}
}