Compare commits

...

436 Commits

Author SHA1 Message Date
Andrew Pareles 3de9faed5c fix katex? 2025-04-21 16:37:31 -07:00
Andrew Pareles c179912368 ajv 2025-04-21 14:45:20 -07:00
Andrew Pareles 89c2adee70 Merge pull request #424 from voideditor/model-selection
update package-lock??
2025-04-20 20:32:27 -07:00
Andrew Pareles c4b9637546 update package-lock?? 2025-04-20 20:32:04 -07:00
Andrew Pareles 868736330a Merge pull request #423 from voideditor/model-selection
Fix build?
2025-04-20 20:20:54 -07:00
Andrew Pareles 2c856c9932 rm google vertex and fast-json-stable-stringify, and revert package-lock to try to fix build 2025-04-20 20:09:48 -07:00
Andrew Pareles a3b49d8aa2 add extensions transfer omits 2025-04-20 19:04:56 -07:00
Andrew Pareles da9817bd99 misc 2025-04-20 18:44:07 -07:00
Andrew Pareles 58b4d90b49 changeDiff prompt, chat thread state, improve import/export 2025-04-20 18:32:13 -07:00
Andrew Pareles 39ea63b207 add download/upload/reset 2025-04-20 06:43:49 -07:00
Andrew Pareles b02b2f0c89 duplicate thread, retry, search_in_file 2025-04-20 04:22:05 -07:00
Mathew Pareles 902d419026 at to mention draft 2025-04-20 02:06:35 -07:00
Andrew Pareles b244f2f08b tooltips 2025-04-20 01:54:44 -07:00
Andrew Pareles 4d9b52ede6 loader and misc 2025-04-20 01:13:47 -07:00
Andrew Pareles e94d2de641 bgTerminalId !== null 2025-04-19 23:59:12 -07:00
Andrew Pareles 5fdc5d6df2 Merge pull request #421 from voideditor/model-selection
fix ajv missing dep
2025-04-19 17:10:12 -07:00
Andrew Pareles cf5ff98081 fix ajv missing dep 2025-04-19 17:07:59 -07:00
Andrew Pareles 45eeb14b54 Merge pull request #420 from voideditor/model-selection
package-lock fix
2025-04-19 16:28:11 -07:00
Andrew Pareles dcfb5d443c fix package-lock? 2025-04-19 16:27:39 -07:00
Andrew Pareles f839e73ebc @types/katex as dev dep 2025-04-19 00:26:47 -07:00
Andrew Pareles c775d34d75 Merge pull request #417 from voideditor/model-selection
Misc updates
2025-04-18 20:58:55 -07:00
Andrew Pareles 3023ed87d2 1.2.6 2025-04-18 20:58:43 -07:00
Andrew Pareles 3a5a04ceae Update README.md 2025-04-18 20:54:57 -07:00
Andrew Pareles 22475f7206 Update README.md 2025-04-18 20:54:40 -07:00
Andrew Pareles 68333ce349 Update README.md 2025-04-18 20:54:01 -07:00
Andrew Pareles 9f3dbc3eb4 Update VOID_CODEBASE_GUIDE.md 2025-04-18 20:52:13 -07:00
Andrew Pareles 10949b44d1 add retries, fix key, X on autodetected 2025-04-18 20:48:57 -07:00
Andrew Pareles 45778d5735 Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:49:17 -07:00
Andrew Pareles 816d6090cc Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:46:04 -07:00
Andrew Pareles 355d8d9a5a Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:41:06 -07:00
Andrew Pareles b47e8f2237 Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:14:36 -07:00
Andrew Pareles 0543b2c893 Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:11:38 -07:00
Andrew Pareles 69cf58af5a Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:06:25 -07:00
Andrew Pareles f9b3dbe316 Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:05:48 -07:00
Andrew Pareles 176b988539 Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:04:21 -07:00
Andrew Pareles 11cc9f8b4e Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:03:33 -07:00
Andrew Pareles ab396d4c1d Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:02:22 -07:00
Andrew Pareles a01ca97b18 Update VOID_CODEBASE_GUIDE.md 2025-04-18 18:01:09 -07:00
Andrew Pareles 1b4105ed2b Update issue_template.md 2025-04-18 14:57:16 -07:00
Andrew Pareles 4024ed40c2 Update issue_template.md 2025-04-18 14:56:56 -07:00
Andrew Pareles 8c53878681 Update issue_template.md 2025-04-18 14:56:33 -07:00
Andrew Pareles 7bd52438f5 try/catch voidrules 2025-04-18 06:24:33 -07:00
Andrew Pareles 6f2d513d3f Delete .github/issue_template.md 2025-04-18 06:19:54 -07:00
Andrew Pareles 800aeea247 misc fixes 2025-04-18 06:08:54 -07:00
Mathew Pareles 021acf541a latex works with some hacks 2025-04-18 05:42:35 -07:00
Andrew Pareles 7a739e567c prepare retry loop 2025-04-18 04:44:14 -07:00
Andrew Pareles ef52ebfdc2 2.5 pro 2025-04-18 04:03:32 -07:00
Andrew Pareles e83a172816 reorder 2025-04-18 04:00:01 -07:00
Andrew Pareles c3ef7e616f lm studio reasoning! 2025-04-18 03:59:23 -07:00
Andrew Pareles f77907fe0d LM studio refreshing 2025-04-18 03:48:44 -07:00
Andrew Pareles 6d28b0627f add GCP vertex (w/ auth), MSFT azure, litellm, lm studio - NEEDS TESTING 2025-04-18 03:48:44 -07:00
Andrew Pareles 7d3ce48f5f extract \n from FIM 2025-04-18 03:48:44 -07:00
Andrew Pareles 97c611f4f2 improve Fast Apply prompt 2025-04-18 03:48:44 -07:00
Mathew Pareles 90d94ecd3c ux 2025-04-18 01:03:08 -07:00
Mathew Pareles 9f7154ddab table 2025-04-18 00:11:36 -07:00
Mathew Pareles 5cdb4b0c47 ux 2025-04-17 23:35:23 -07:00
Mathew Pareles a61fdd0419 ux 2025-04-17 23:32:01 -07:00
Mathew Pareles b7db527866 history ux 2025-04-17 22:41:40 -07:00
Mathew Pareles c921059690 thread history ux 2025-04-17 21:27:21 -07:00
Mathew Pareles cee6dcc524 rename .voidinstructions to .voidrules ... 2025-04-17 21:27:21 -07:00
Andrew Pareles b9e097bfc0 Update issue_template.md 2025-04-17 18:46:58 -07:00
Andrew Pareles 87c6669020 Update issue_template.md 2025-04-17 18:46:42 -07:00
Andrew Pareles 80448c4471 Update issue_template.md 2025-04-17 18:44:57 -07:00
Andrew Pareles b4f9c1fa97 Delete .github/issue_template.md 2025-04-17 18:43:57 -07:00
Andrew Pareles 10052819ce Update issue_template.md 2025-04-17 18:43:44 -07:00
Andrew Pareles 0f2ebecbd3 Create issue_template.md 2025-04-17 18:39:58 -07:00
Andrew Pareles eb68e1d294 Update issue_template.md 2025-04-17 18:38:55 -07:00
Andrew Pareles 17aa9eea97 prompt 2025-04-17 18:29:37 -07:00
Andrew Pareles 5e85d804f8 update 2025-04-17 17:30:07 -07:00
Andrew Pareles 8f2a2d642f Merge pull request #415 from voideditor/model-selection
Template
2025-04-17 17:22:41 -07:00
Andrew Pareles 6141351e15 issue template 2025-04-17 17:21:53 -07:00
Andrew Pareles 00e196a55f Update HOW_TO_CONTRIBUTE.md 2025-04-17 15:54:16 -07:00
Andrew Pareles 16913e96e5 prompt 2025-04-16 23:44:24 -07:00
Andrew Pareles 6f55e6900b Merge pull request #413 from voideditor/model-selection
1.2.5 misc
2025-04-16 22:57:50 -07:00
Andrew Pareles 36e10dcc08 1.2.5 2025-04-16 22:56:52 -07:00
Andrew Pareles ce8c790e5c undo version 2025-04-16 22:47:52 -07:00
Mathew Pareles 738067951a ux 2025-04-16 22:31:28 -07:00
Andrew Pareles 8b1a42499b add more boundaries 2025-04-16 21:56:23 -07:00
Mathew Pareles 7fd52b4805 fix commandbarinchat state 2025-04-16 20:44:47 -07:00
Mathew Pareles 5f30506f46 terminal tool style 2025-04-16 20:44:47 -07:00
Andrew Pareles 48efc32dd3 lint finite size 2025-04-16 20:25:12 -07:00
Andrew Pareles 5fffd2fe0f rename command tool 2025-04-16 20:23:52 -07:00
Andrew Pareles 664428c8f0 add download chat buttons 2025-04-16 18:09:03 -07:00
Andrew Pareles aa8b999da0 Merge pull request #409 from voideditor/model-selection
prompt update for o3
2025-04-16 14:31:10 -07:00
Andrew Pareles d99d2cca4a prompt update for o3 2025-04-16 14:30:49 -07:00
Andrew Pareles e7da1d27bd Merge pull request #408 from voideditor/model-selection
o3 fallback
2025-04-16 10:47:31 -07:00
Andrew Pareles 56c50dc218 fallback 2025-04-16 10:46:37 -07:00
Andrew Pareles e0e7b360fe Merge pull request #407 from voideditor/model-selection
Model selection
2025-04-16 10:41:03 -07:00
Andrew Pareles 434a8289d9 1.2.4 bump 2025-04-16 10:40:37 -07:00
Andrew Pareles 7e0e65d3b7 o3, o4-mini 2025-04-16 10:39:37 -07:00
Andrew Pareles 4cb96cb60e Merge pull request #403 from Dylan-86/patch-2
Fix AppImage creation bug
2025-04-16 04:30:39 -07:00
Andrew Pareles 4a7ea3ec43 appimage 2025-04-16 04:30:01 -07:00
Andrew Pareles bd53772523 Merge remote-tracking branch 'origin/main' into pr/Dylan-86/403 2025-04-16 04:29:58 -07:00
Andrew Pareles f09f538b05 Merge pull request #406 from voideditor/model-selection
Onboarding, SSH
2025-04-16 04:27:13 -07:00
Andrew Pareles ff2ed836c7 onboarding style 2025-04-16 04:26:35 -07:00
Mathew Pareles 6db29526ec add letterpress image 2025-04-16 04:04:50 -07:00
Andrew Pareles 27d9cc2916 comment 2025-04-16 03:50:56 -07:00
Andrew Pareles dbbaa61961 try again 2025-04-16 03:49:41 -07:00
Andrew Pareles 05bd1790a7 mistral 2025-04-16 03:46:14 -07:00
Mathew Pareles 4b30990e92 fix long api key 2025-04-16 03:35:25 -07:00
Mathew Pareles 3a0eedc9fa py 2025-04-16 03:29:15 -07:00
Mathew Pareles 64d4e264bf onboarding 2025-04-16 03:27:44 -07:00
Mathew Pareles afc5cf512b remove tooltip for now 2025-04-16 03:21:14 -07:00
Mathew Pareles 35e8cf59ff better ending screen onboarding 2025-04-16 03:14:52 -07:00
Mathew Pareles fd4ecaff8a fz 2025-04-16 02:51:42 -07:00
Mathew Pareles 55c4f9e1cf onboarding decent final draft 2025-04-16 02:47:19 -07:00
Andrew Pareles 78f9463da1 .voidinstructions 2025-04-16 02:27:29 -07:00
Andrew Pareles 23f1e8c641 .voidinstructions file 2025-04-16 02:21:36 -07:00
Andrew Pareles 79eacad18b error boundaries everywhere + misc (BROKEN) 2025-04-16 02:21:17 -07:00
Andrew Pareles 3e0e0bd4aa SSH buttons 2025-04-16 01:23:25 -07:00
Andrew Pareles fece3b2548 update settings page 2025-04-16 01:18:59 -07:00
Andrew Pareles 091855a89a linux version for deb, rem, snap 2025-04-16 01:02:57 -07:00
Andrew Pareles b3bd454bad add SSH button 2025-04-16 01:02:57 -07:00
Mathew Pareles bd580946ce add image 2025-04-16 00:58:24 -07:00
Mathew Pareles 3bd6d08841 button 2025-04-16 00:58:24 -07:00
Andrew Pareles 152cfd93aa Merge pull request #404 from voideditor/model-selection
1.2.1 updates
2025-04-15 23:09:44 -07:00
Andrew Pareles 7ea4e16f5f 1.2.3 2025-04-15 23:08:26 -07:00
Andrew Pareles 884548615b checkpoint UX and dropdown UI 2025-04-15 23:06:13 -07:00
Mathew Pareles b62943fffd tooltip for ollama 2025-04-15 18:35:23 -07:00
Andrew Pareles 9bd97d0cd5 Claude reasoning small fix 2025-04-15 17:30:35 -07:00
Andrew Pareles bf9852858e remove auto-selection of current file 2025-04-15 17:30:21 -07:00
Andrew Pareles 6058b7ff78 misc fixes and gemini prompt improvement 2025-04-15 17:04:06 -07:00
Mathew Pareles 9b8f3ec2e2 desc 2025-04-15 16:52:21 -07:00
Mathew Pareles d3fd5be294 small 2025-04-15 16:38:54 -07:00
Mathew Pareles aa4398c734 small changes 2025-04-15 16:36:46 -07:00
Mathew Pareles 29d1a919be small styles 2025-04-15 16:16:04 -07:00
Andrew Pareles feea5e13f3 update service fix 2025-04-15 15:56:15 -07:00
Mathew Pareles 119db73f69 colors 2025-04-15 15:12:40 -07:00
Andrew Pareles f59b24496e 1.2.2 fix gemini 2025-04-15 15:01:24 -07:00
Andrew Pareles a2ae9794ed eslint 2025-04-15 15:00:44 -07:00
Andrew Pareles f335ce7204 fix gemini 2025-04-15 14:59:32 -07:00
Mathew Pareles 0ddab03f8b styles 2025-04-15 03:23:31 -07:00
Mathew Pareles 5f8fddb01e fix flash 2025-04-15 03:23:31 -07:00
Mathew Pareles c81f628b7b colors 2025-04-15 03:23:31 -07:00
Mathew Pareles 9d1ab8728a styles 2025-04-15 03:23:31 -07:00
Dylan-86 7a0f9ac77c Fix AppImage creation bug
See https://github.com/voideditor/void/issues/402
2025-04-15 12:22:15 +02:00
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
Andrew Pareles d5dc411e4c Merge remote-tracking branch 'origin/main' into tools-in-plaintext 2025-04-09 08:16:51 -07:00
Andrew Pareles e14fdd6678 awaited 2025-04-09 08:16:43 -07:00
Andrew Pareles 8b5dfa66d5 Merge remote-tracking branch 'origin/model-selection' into tools-in-plaintext 2025-04-09 08:14:01 -07:00
Andrew Pareles aef606dfb4 better update checking 2025-04-09 08:08:47 -07:00
Mathew Pareles 89b29ac97b settings page is decent for update 2025-04-09 07:56:17 -07:00
Andrew Pareles 908de32b8f Merge pull request #382 from voideditor/revert-374-main
Revert "Merge changes from vscode disabling mangling"
2025-04-09 07:08:46 -07:00
Andrew Pareles 4787400865 Revert "Merge changes from vscode disabling mangling" 2025-04-09 07:08:30 -07:00
Mathew Pareles 8b5e7dc25b revert package.json bug 2025-04-09 06:22:41 -07:00
Andrew Pareles 7289f5f15a Merge remote-tracking branch 'origin/main' into tools-in-plaintext 2025-04-09 06:09:16 -07:00
Jérôme Commaret 5e83d419ce Merge Main and add English Comments 2025-04-09 13:47:33 +02:00
Mathew Pareles 3128000cf7 fix ux 2025-04-09 04:39:32 -07:00
Jérôme Commaret b1626849ba Merge branch 'main' into feat_mistral-199 2025-04-09 13:33:06 +02:00
Mathew Pareles 99901d1e20 prepare to add linterror ui (incomplete) 2025-04-09 04:31:06 -07:00
Andrew Pareles 858faed92b 5s 2025-04-09 04:18:46 -07:00
Andrew Pareles a4f94087df supportsTools is back 2025-04-09 04:01:39 -07:00
Andrew Pareles 4236d920d9 Merge remote-tracking branch 'origin/model-selection' into tools-in-plaintext 2025-04-09 04:01:30 -07:00
Andrew Pareles 56619c7b17 rm copilot head 2025-04-09 03:31:34 -07:00
Andrew Pareles ea65903b3f fix running_tool state and double undo on click stop in Apply 2025-04-09 03:25:09 -07:00
Andrew Pareles 78671db5b8 checkpoint state improvements 2025-04-09 02:45:34 -07:00
Mathew Pareles 7f38c1c9c3 ux 2025-04-09 01:23:31 -07:00
Mathew Pareles eefa9648c4 ui changes 2025-04-09 01:10:32 -07:00
Andrew Pareles 8c26fd2081 params 2025-04-08 23:59:07 -07:00
Andrew Pareles 49fbb62259 proper handling of tool call result, and improved prompting 2025-04-08 23:55:57 -07:00
Andrew Pareles ad5a77dc55 rm sax 2025-04-08 15:43:06 -07: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
Andrew Pareles 052a50f9b0 misc fixes + clarify displayContent 2025-04-08 03:20:34 -07:00
Andrew Pareles e331fb4eed trim whitespace + fix onFinalMessage last chunk error 2025-04-08 02:44:05 -07:00
Andrew Pareles 4e197f4659 debug 2025-04-08 02:04:08 -07:00
Andrew Pareles 39bf2283cc GPTify sax library 2025-04-08 02:03:25 -07:00
Mathew Pareles c87d1886cd tooltips work 2025-04-08 02:00:42 -07:00
Andrew Pareles 6f693c4d0a progress 2025-04-07 23:05:03 -07:00
Andrew Pareles 1c5adb96d3 tool calls via plaintext initial draft 2025-04-07 22:26:31 -07:00
Andrew Pareles 8f8fa8548d + 2025-04-07 20:28:39 -07:00
Mathew Pareles bbe7575321 fix styles that were broken by someone 2025-04-07 18:36:51 -07:00
Andrew Pareles dae5d054e7 Merge pull request #371 from bjoaquinc/remote-wsl-extension
Added open-remote-wsl to /extensions
2025-04-07 16:49:56 -07:00
Andrew Pareles 0638cef79b Merge pull request #374 from GhostDog98/main
Merge changes from vscode disabling mangling
2025-04-07 16:49:12 -07:00
Mathew Pareles 8e2e8c58c9 onboarding draft and lots of misc changes 2025-04-07 16:38:15 -07:00
Mathew Pareles 4b4dde71b3 bug 2025-04-07 16:38:15 -07:00
Mathew Pareles d59b99a2a0 make helper close when Cmd+L or Cmd+K pressed 2025-04-07 16:38:15 -07:00
Mathew Pareles df32898523 remove dropdown max w 2025-04-07 16:38:15 -07:00
Mathew Pareles 2bd8e61706 fn 2025-04-07 16:38:15 -07:00
Mathew Pareles 35724f36cd smartselect draft (need to add setting) 2025-04-07 16:38:15 -07:00
Mathew Pareles 7fba51b59f final changes to selection helper 2025-04-07 16:38:15 -07:00
Mathew Pareles 599d80dcc3 minor change 2025-04-07 16:38:15 -07:00
Mathew Pareles 3c186031be minor positional change 2025-04-07 16:38:15 -07:00
Mathew Pareles 51c84df5a5 better helperwidget positioning 2025-04-07 16:38:15 -07:00
Andrew Pareles 77c031abc5 300ms 2025-04-07 16:13:18 -07:00
Andrew Pareles e81e71c81f hover delay 300ms 2025-04-07 16:12:37 -07:00
Andrew Pareles be07ecec2e Merge remote-tracking branch 'origin/main' into model-selection 2025-04-07 04:00:57 -07:00
Andrew Pareles 3aa0bf11e6 Merge pull request #337 from Dylan-86/main
Added more models for agents from openRouter
2025-04-07 03:46:40 -07:00
Andrew Pareles 329e68ccb1 merge 2025-04-07 03:46:12 -07:00
Andrew Pareles f3da41ba46 Merge pull request #376 from GhostDog98/patch-2
Update CONTRIBUTING.md
2025-04-07 03:39:10 -07:00
Andrew Pareles 78384b3acf Merge pull request #378 from voideditor/add-checkpoints
Add checkpoints
2025-04-07 03:38:31 -07:00
Andrew Pareles 2912d5cd89 rm logs 2025-04-07 03:37:43 -07:00
Andrew Pareles c699577183 only add current file if no message was sent 2025-04-07 02:51:46 -07:00
Andrew Pareles 707fe9e0a3 add links for any file the LLM has read 2025-04-07 02:40:27 -07:00
Andrew Pareles 71dd603c3d context weights 2025-04-07 01:59:45 -07:00
Andrew Pareles 7aa5f1e4ec context trimming 2025-04-07 01:46:07 -07:00
Andrew Pareles 7c4e92a030 pass lint errors!! 2025-04-06 22:47:57 -07:00
Andrew Pareles 76125cc83e feed lint errors 2025-04-06 22:46:30 -07:00
Andrew Pareles 3fc0fd0bc0 ghost 2025-04-06 22:16:32 -07:00
Andrew Pareles 5b0d8f4418 tool use + checkpoint state updates 2025-04-06 22:08:00 -07:00
ghostoverflow256 d71f377ac1 Update CONTRIBUTING.md
also this
2025-04-07 11:23:13 +10:00
ghostoverflow256 dbd63229c8 Update CONTRIBUTING.md
Also linux ia32 doesn't exist as a build target in the gulp files...
2025-04-07 11:21:37 +10:00
ghostoverflow256 1e39cdaba4 Update CONTRIBUTING.md
`vscode-linux-arm` does not exist as a defined build option. `vscode-linux-arm64` *does* however.
2025-04-07 11:13:43 +10:00
Andrew Pareles 5f76fa8d7d turn off reasoning for everything except chat by default 2025-04-06 17:49:32 -07:00
Andrew Pareles f6358e319b model capabilities progress 2025-04-06 17:31:11 -07:00
Andrew Pareles 5a52689d91 Merge pull request #375 from GhostDog98/patch-1
Update CONTRIBUTING.md
2025-04-06 17:04:11 -07:00
ghostoverflow256 5187f0e96c Update CONTRIBUTING.md
Added fix for macos compilation
2025-04-07 09:29:29 +10:00
João Moreno 086e471dd5 disable mangling when building vscode locally (#243856)
* disable mangling when building vscode locally

* local build: ensure mangling runs when minification is on
2025-04-07 09:06:17 +10:00
Andrew Pareles 033de587f2 improve checkpoint logic + UI 2025-04-06 03:29:45 -07:00
Andrew Pareles bd0db41f67 compare by ignoring whitespace 2025-04-05 21:13:03 -07:00
Andrew Pareles a5ac79c6a4 gemma 2025-04-05 21:06:27 -07:00
Andrew Pareles 67a253164e + 2025-04-05 20:56:28 -07:00
Andrew Pareles 3c93e0a414 separate out thinking by feature 2025-04-05 20:56:08 -07:00
Andrew Pareles 1d9e0faaa3 make chats by reference 2025-04-05 20:40:59 -07:00
Andrew Pareles b440c8732f search tools now support regex, reading line numbers, etc 2025-04-05 16:59:03 -07:00
Andrew Pareles 2aae01f3b6 rename tools 2025-04-05 15:59:43 -07:00
Andrew Pareles c1ffca04a7 recursive folder tool call and context 2025-04-05 00:26:20 -07:00
Andrew Pareles de703e82cb add new models, more info in prompt, 2025-04-04 02:39:11 -07:00
Andrew Pareles a2a9cdba60 checkpoints revert/restore diffareas!!! 2025-04-03 17:12:18 -07:00
Jérôme Commaret 795ce62ce8 fix mistral tooling 2025-04-03 20:55:30 +02:00
Andrew Pareles bde51106a1 checkpoints 2025-04-03 01:03:03 -07:00
Andrew Pareles 3ac9dcf0c0 checkpoint progress 2025-04-02 19:13:51 -07:00
Jérôme Commaret 2d9fe3b0be fix models capabilities 2025-04-02 23:39:44 +02:00
Andrew Pareles 7e8af9c2ef improve Apply state and misc other improvements for Apply 2025-04-02 00:25:23 -07:00
Andrew Pareles a94ce5d474 checkpoints work! need to add user_temp_modifications now and deal with diffzones 2025-03-31 22:56:47 -07:00
Joaquin Coromina eceaa6a59b Updated DEFAULT_DOWNLOAD_URL_TEMPLATE to also point at the void binaries repo 2025-04-01 00:03:02 -04:00
Joaquin Coromina 103dd68b3c Updated config to point at voidVersion instead of the base vscode version 2025-04-01 00:00:34 -04:00
Andrew Pareles 7decc8e146 add checkpoints 2025-03-31 20:35:40 -07:00
Joaquin Coromina c58210ba80 Updated the package.json default url to point to the void binaries repo properly 2025-03-31 23:16:06 -04:00
Joaquin Coromina c68a497e77 Merge branch 'voideditor:main' into remote-wsl-extension 2025-03-31 23:11:16 -04:00
Jérôme Commaret 03992c1206 Merge branch 'voideditor:main' into main 2025-03-31 16:48:53 +02:00
Andrew Pareles 7c0ba71314 scaffolding for checkpoints 2025-03-30 21:30:01 -07:00
Andrew Pareles 878a439acd rm tool test thread 2025-03-30 20:53:07 -07:00
Andrew Pareles f047ef9c55 add dummy contrib 2025-03-30 20:52:00 -07:00
Andrew Pareles db6ebeb9f2 Merge pull request #364 from bjoaquinc/patch-2
Updated version argument to get voidVersion
2025-03-30 20:47:06 -07:00
Andrew Pareles 7631b41b27 comment 2025-03-30 20:45:42 -07:00
Andrew Pareles b2d8ca3023 Merge pull request #365 from voideditor/minor-changes-mar
Minor changes mar
2025-03-30 20:44:53 -07:00
Joaquin Coromina 0294cb3b98 Removed the fall back, restored the original code and commented it out 2025-03-30 22:51:40 -04:00
Joaquin Coromina 88dc32a43e Merge branch 'main' into patch-2 2025-03-30 22:46:27 -04:00
Joaquin Coromina 42e1277bb9 Merge branch 'voideditor:main' into remote-wsl-extension 2025-03-30 20:30:43 -04:00
Andrew Pareles 2dcd95cf2a Merge pull request #363 from bjoaquinc/patch-1
Fixed default url in package.json since this is used by app
2025-03-30 17:24:37 -07:00
Joaquin Coromina 499abdf26c Fixed serverConfig.ts to pull voidVersion instead of version 2025-03-30 18:40:41 -04:00
Joaquin Coromina 52e92ac3f9 Update ${NAME_OF_REPO} to binaries repo for accessing the remote host tarballs 2025-03-30 10:50:40 -04:00
Andrew Pareles 885ebc8528 Merge pull request #360 from bjoaquinc/updated-remote-ssh-for-binaries
Fixed the open-remote-ssh extension to point at binaries repo
2025-03-30 03:50:07 -07:00
Andrew Pareles 4f4ef694ff Merge pull request #362 from voideditor/minor-changes-mar
1.0.4 test
2025-03-29 22:01:10 -07:00
Andrew Pareles 57795b3210 Merge pull request #361 from voideditor/minor-changes-mar
Minor changes mar
2025-03-29 21:21:46 -07:00
Jérôme Commaret 0c30c2681d Merge branch 'voideditor:main' into main 2025-03-30 00:23:24 +01:00
Joaquin Coromina 36277eaed8 updated the downlaod url to point at void reh 2025-03-29 17:43:12 -04:00
Joaquin Coromina ba4cac3fb6 Updated the DEFAULT_DOWNLOAD_URL_TEMPLATE to point at the new void binaries repo and made the string dynamic 2025-03-29 17:34:29 -04:00
Andrew Pareles 3ce2911e3d 1.0.3 2025-03-29 03:39:48 -07:00
Andrew Pareles 89dc3c8805 1.0.2 2025-03-29 03:38:19 -07:00
Andrew Pareles 1bbd6f59fe Merge pull request #358 from voideditor/minor-changes-mar
version -> voidVersion
2025-03-29 03:34:36 -07:00
Mathew Pareles 7fdde7af3e inline suggestions finished 2025-03-29 03:16:30 -07: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
Mathew Pareles 6a73930f4b small styling change 2025-03-26 04:13:07 -07:00
Mathew Pareles 6c382be737 styles 2025-03-26 03:49:25 -07:00
Mathew Pareles c43a234788 commandbar (needs styling for accept/reject) 2025-03-26 03:47:45 -07: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
Dylan-86 24e5b78583 Added more models for agents from openRouter 2025-03-25 12:09:39 +01:00
Andrew Pareles ed89858fbc leading-1 2025-03-25 01:26:41 -07:00
Jérôme Commaret e0aee9863e ADD : closing the notification update 2025-03-24 16:26:16 +01:00
Joaquin Coromina 016d59e81a Merge branch 'voideditor:main' into remote-wsl-extension 2025-03-23 20:03:22 -04: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
Joaquin Coromina 9fce7ce812 Merge branch 'voideditor:main' into remote-wsl-extension 2025-03-08 19:20:59 +07:00
Joaquin Coromina 85009ab5b3 Added open-remote-wsl output folder to the exclude list for hygiene checks 2025-03-08 05:48:15 -06:00
Joaquin Coromina fe01285d58 Added open-remote-wsl to the npm install build command 2025-03-08 05:42:20 -06:00
Joaquin Coromina d1fa4d3439 Added open-remote-wsl to the gulp build pipeline 2025-03-08 05:39:43 -06:00
Joaquin Coromina d0921f899c Added open-remote-wsl to extensions 2025-03-08 00:23:54 -06:00
113 changed files with 19472 additions and 8509 deletions
+9
View File
@@ -0,0 +1,9 @@
---
name: Issue
about: Submit an Issue to Void
title: For VSCode-related issues (eg builds), please start the title with `[App]`. Otherwise, start it with `[Bug]` or `[Feature]`.
---
1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable.
2. Describe the issue/feature here.
+6 -9
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/`.
@@ -90,7 +88,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
- If you see missing styles, wait a few seconds and then reload.
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
## Packaging
@@ -104,12 +102,11 @@ We don't usually recommend packaging. Instead, you should probably just build. I
### Windows
- `npm run gulp vscode-win32-x64` - most common
- `npm run gulp vscode-win32-ia32`
- `npm run gulp vscode-win32-arm64`
### Linux
- `npm run gulp vscode-linux-x64` - most common
- `npm run gulp vscode-linux-arm`
- `npm run gulp vscode-linux-ia32`
- `npm run gulp vscode-linux-arm64`
### Output
@@ -122,7 +119,7 @@ workspace/
```
### Distributing
Void's maintainers distribute Void on our website and in releases. If you'd like to see the scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution, feel free to reach out.
Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor/void-builder).
## Pull Request Guidelines
+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 [`HOW_TO_CONTRIBUTE`](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 [`VOID_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.
+171
View File
@@ -0,0 +1,171 @@
# 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 instead, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
## Void Codebase Guide
### VSCode Rundown
Here's a VSCode rundown if you're just getting started with Void. You can also see Microsoft's [wiki](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for some pictures. VSCode 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").
<p align="center" >
<img src="https://github.com/user-attachments/assets/eef80306-2bfe-4cac-ba15-6156f65ab3bb" alt="Credit - https://github.com/microsoft/vscode/wiki/Source-Code-Organization" width="700px">
</p>
- 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`. We came up with 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.
### Terminology
Here's some terminology you might want to know about when 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.
<p align="center" >
<img src="https://github.com/user-attachments/assets/6521c228-dc96-4cf5-a673-6b9ca78b9b06" alt="Credit - https://code.visualstudio.com/docs/getstarted/userinterface" width="400px">
</p>
- 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.
- "**Actions**" are functions you register on VSCode so that either you or the user can call them later. They're also called "**Commands**".
- 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.
### 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 of text where we compute and show red/green areas, or **Diffs**. When any changes are made to a file, we loop through all the DiffAreas on that file and refresh its Diffs.
- A **DiffArea** is a generalization that just tracks line numbers like a DiffZone.
- The only type of DiffArea that can "stream" is a DiffZone. Each DiffZone has an llmCancelToken if it's streaming.
How Apply works:
- 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
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
View File
@@ -139,6 +139,7 @@ module.exports.indentationFilter = [
'!extensions/simple-browser/media/*.js',
'!extensions/open-remote-ssh/out/*.js', // Void added this
'!extensions/open-remote-wsl/out/*.js', // Void added this
];
module.exports.copyrightFilter = [
+1
View File
@@ -73,6 +73,7 @@ const compilations = [
'.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json',
'extensions/open-remote-ssh/tsconfig.json', // Void added this
'extensions/open-remote-wsl/tsconfig.json', // Void added this
];
+1
View File
@@ -55,6 +55,7 @@ const dirs = [
'.vscode/extensions/vscode-selfhost-import-aid',
'.vscode/extensions/vscode-selfhost-test-provider',
'extensions/open-remote-ssh', // Void added this
'extensions/open-remote-wsl', // Void added this
];
+1 -1
View File
@@ -71,7 +71,7 @@
"type": "string",
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
"scope": "application",
"default": "https://github.com/voideditor/${NAME_OF_REPO}/releases/download/${version}.${release}/void-server-${os}-${arch}-${version}.${release}.tar.gz"
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
},
"remote.SSH.remotePlatform": {
"type": "object",
@@ -32,12 +32,14 @@ export async function getVSCodeServerConfig(): Promise<IServerConfig> {
const customServerBinaryName = vscode.workspace.getConfiguration('remote.SSH.experimental').get<string>('serverBinaryName', '');
return {
version: vscode.version.replace('-insider', ''),
// version: vscode.version.replace('-insider', ''),
commit: productJson.commit,
quality: productJson.quality,
release: productJson.release,
serverApplicationName: customServerBinaryName || productJson.serverApplicationName,
serverDataFolderName: productJson.serverDataFolderName,
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate,
// Void changed this
version: productJson.voidVersion
};
}
@@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/void-updates-server/releases/download/test/void-server-${os}-${arch}.tar.gz';
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
let shell = 'powershell';
+3
View File
@@ -0,0 +1,3 @@
# Remote - WSL Support
Inherited for Void from [Open Remote - WSL](https://github.com/jeanp413/open-remote-wsl).
@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withBrowserDefaults = require('../shared.webpack.config').browser;
module.exports = withBrowserDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts'
}
});
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
resolve: {
mainFields: ['module', 'main']
},
entry: {
extension: './src/extension.ts',
}
});
+15
View File
@@ -0,0 +1,15 @@
{
"name": "open-remote-wsl",
"version": "0.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-remote-wsl",
"version": "0.0.4",
"engines": {
"vscode": "^1.70.2"
}
}
}
}
+281
View File
@@ -0,0 +1,281 @@
{
"name": "open-remote-wsl",
"displayName": "Remote - WSL",
"description": "Open any folder in the Windows Subsystem for Linux (WSL).",
"version": "0.0.4",
"icon": "resources/icon.png",
"engines": {
"vscode": "^1.70.2"
},
"extensionKind": [
"ui"
],
"enabledApiProposals": [
"resolvers",
"contribViewsRemote"
],
"keywords": [
"remote development",
"remote",
"wsl"
],
"api": "none",
"activationEvents": [
"onCommand:openremotewsl.connect",
"onCommand:openremotewsl.connectInNewWindow",
"onCommand:openremotewsl.connectUsingDistro",
"onCommand:openremotewsl.connectUsingDistroInNewWindow",
"onCommand:openremotewsl.showLog",
"onResolveRemoteAuthority:wsl",
"onView:wslTargets"
],
"main": "./out/extension.js",
"contributes": {
"configuration": {
"title": "WSL",
"properties": {
"remote.WSL.serverDownloadUrlTemplate": {
"type": "string",
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
"scope": "application",
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
}
}
},
"views": {
"remote": [
{
"id": "wslTargets",
"name": "WSL Targets",
"group": "targets@1",
"when": "(isWindows && !isWeb)",
"remoteName": "wsl"
}
]
},
"commands": [
{
"command": "openremotewsl.connect",
"title": "Connect to WSL",
"category": "Remote-WSL"
},
{
"command": "openremotewsl.connectInNewWindow",
"title": "Connect to WSL in New Window",
"category": "Remote-WSL"
},
{
"command": "openremotewsl.connectUsingDistro",
"title": "Connect to WSL using Distro...",
"category": "Remote-WSL"
},
{
"command": "openremotewsl.connectUsingDistroInNewWindow",
"title": "Connect to WSL using Distro in New Window...",
"category": "Remote-WSL"
},
{
"command": "openremotewsl.showLog",
"title": "Show Log",
"category": "Remote-WSL"
},
{
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
"title": "Connect in New Window",
"icon": "$(empty-window)"
},
{
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
"title": "Connect in Current Window"
},
{
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
"title": "Open in Current Window"
},
{
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
"title": "Open in New Window",
"icon": "$(folder-opened)"
},
{
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
"title": "Remove From Recent List",
"icon": "$(x)"
},
{
"command": "openremotewsl.explorer.refresh",
"title": "Refresh",
"icon": "$(refresh)"
},
{
"command": "openremotewsl.explorer.addDistro",
"title": "Add a Distro",
"icon": "$(plus)"
},
{
"command": "openremotewsl.explorer.setDefaultDistro",
"title": "Set as Default Distro"
},
{
"command": "openremotewsl.explorer.deleteDistro",
"title": "Delete Distro"
}
],
"resourceLabelFormatters": [
{
"scheme": "vscode-remote",
"authority": "wsl+*",
"formatting": {
"label": "${path}",
"separator": "/",
"tildify": true,
"workspaceSuffix": "WSL"
}
}
],
"menus": {
"statusBar/remoteIndicator": [
{
"command": "openremotewsl.connect",
"when": "(isWindows && !isWeb)",
"group": "remote_20_wsl_1general@1"
},
{
"command": "openremotewsl.connectUsingDistro",
"when": "(isWindows && !isWeb)",
"group": "remote_20_wsl_1general@2"
},
{
"command": "openremotewsl.showLog",
"when": "remoteName =~ /^wsl$/",
"group": "remote_20_wsl_1general@4"
}
],
"commandPalette": [
{
"command": "openremotewsl.connect",
"when": "(isWindows && !isWeb)"
},
{
"command": "openremotewsl.connectInNewWindow",
"when": "(isWindows && !isWeb)"
},
{
"command": "openremotewsl.connectUsingDistro",
"when": "(isWindows && !isWeb)"
},
{
"command": "openremotewsl.connectUsingDistroInNewWindow",
"when": "(isWindows && !isWeb)"
},
{
"command": "openremotewsl.explorer.refresh",
"when": "false"
},
{
"command": "openremotewsl.explorer.addDistro",
"when": "false"
},
{
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
"when": "false"
},
{
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
"when": "false"
},
{
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
"when": "false"
},
{
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
"when": "false"
},
{
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
"when": "false"
},
{
"command": "openremotewsl.explorer.setDefaultDistro",
"when": "false"
},
{
"command": "openremotewsl.explorer.deleteDistro",
"when": "false"
}
],
"view/title": [
{
"command": "openremotewsl.explorer.addDistro",
"when": "view == wslTargets",
"group": "navigation"
},
{
"command": "openremotewsl.explorer.refresh",
"when": "view == wslTargets",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
"group": "inline@1"
},
{
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
"group": "navigation@2"
},
{
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
"group": "navigation@1"
},
{
"command": "openremotewsl.explorer.setDefaultDistro",
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
"group": "management@1"
},
{
"command": "openremotewsl.explorer.deleteDistro",
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
"group": "management@2"
},
{
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
"when": "viewItem == openremotewsl.explorer.folder",
"group": "inline@1"
},
{
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
"when": "viewItem == openremotewsl.explorer.folder",
"group": "navigation@2"
},
{
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
"when": "viewItem == openremotewsl.explorer.folder",
"group": "navigation@1"
},
{
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
"group": "navigation@3"
},
{
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
"group": "inline@2"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "gulp compile-extension:open-remote-wsl",
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
"watch": "gulp watch-extension:open-remote-wsl",
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
}
}
@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import Log from './common/logger';
import { installCodeServer, ServerInstallError } from './serverSetup';
import { WSLManager } from './wsl/wslManager';
export const REMOTE_WSL_AUTHORITY = 'wsl';
export function getRemoteAuthority(distro: string) {
return `${REMOTE_WSL_AUTHORITY}+${distro}`;
}
class Tunnel implements vscode.Tunnel {
private _onDidDisposeEmitter = new vscode.EventEmitter<void>();
readonly onDidDispose = this._onDidDisposeEmitter.event;
constructor(
readonly remoteAddress: { port: number; host: string },
readonly localAddress: { port: number; host: string }
) {
// If ipv6 localhost 0:0:0:0:0:0:0:1 or [::1] replace with localhost
if (localAddress.host !== 'localhost' && localAddress.host !== '127.0.0.1') {
localAddress.host = 'localhost';
}
}
dispose() {
this._onDidDisposeEmitter.fire();
}
}
export class RemoteWSLResolver implements vscode.RemoteAuthorityResolver, vscode.Disposable {
private labelFormatterDisposable: vscode.Disposable | undefined;
constructor(
private readonly wslManager: WSLManager,
private readonly logger: Log
) {
}
resolve(authority: string, context: vscode.RemoteAuthorityResolverContext): Thenable<vscode.ResolverResult> {
const [type, distroName] = authority.split('+');
if (type !== REMOTE_WSL_AUTHORITY) {
throw new Error(`Invalid authority type for WSL resolver: ${type}`);
}
this.logger.info(`Resolving wsl remote authority '${authority}' (attemp #${context.resolveAttempt})`);
// It looks like default values are not loaded yet when resolving a remote,
// so let's hardcode the default values here
const remoteSSHconfig = vscode.workspace.getConfiguration('remote.WSL');
const serverDownloadUrlTemplate = remoteSSHconfig.get<string>('serverDownloadUrlTemplate');
return vscode.window.withProgress({
title: `Setting up WSL Distro: ${distroName}`,
location: vscode.ProgressLocation.Notification,
cancellable: false
}, async () => {
try {
const installResult = await installCodeServer(this.wslManager, distroName, serverDownloadUrlTemplate, [], [], this.logger);
this.labelFormatterDisposable?.dispose();
this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({
scheme: 'vscode-remote',
authority: `${REMOTE_WSL_AUTHORITY}+*`,
formatting: {
label: '${path}',
separator: '/',
tildify: true,
workspaceSuffix: `WSL: ${distroName}`,
workspaceTooltip: `Running in ${distroName}`
}
});
return new vscode.ResolvedAuthority('127.0.0.1', installResult.listeningOn, installResult.connectionToken);
} catch (e: unknown) {
this.logger.error(`Error resolving authority`, e);
// Initial connection
if (context.resolveAttempt === 1) {
this.logger.show();
const closeRemote = 'Close Remote';
const retry = 'Retry';
const result = await vscode.window.showErrorMessage(`Could not establish connection to WSL distro "${distroName}"`, { modal: true }, closeRemote, retry);
if (result === closeRemote) {
await vscode.commands.executeCommand('workbench.action.remote.close');
} else if (result === retry) {
await vscode.commands.executeCommand('workbench.action.reloadWindow');
}
}
if (e instanceof ServerInstallError || !(e instanceof Error)) {
throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e));
} else {
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message);
}
}
});
}
async tunnelFactory(tunnelOptions: vscode.TunnelOptions) {
return new Tunnel(
tunnelOptions.remoteAddress,
{
host: tunnelOptions.remoteAddress.host,
port: tunnelOptions.localAddressPort ?? tunnelOptions.remoteAddress.port
}
);
}
dispose() {
this.labelFormatterDisposable?.dispose();
}
}
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { getRemoteAuthority } from './authResolver';
import { WSLDistro, WSLManager, WSLOnlineDistro } from './wsl/wslManager';
import wslTerminal from './wsl/wslTerminal';
async function showDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLDistro | undefined> {
const pickItemsPromise = wslManager.listDistros()
.then(distros => distros.map(distroData => {
return {
...distroData,
label: `${distroData.name}`,
detail: distroData.isDefault ? 'default distro' : undefined,
};
}));
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
return picked;
}
async function showOnlineDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLOnlineDistro | undefined> {
const pickItemsPromise = Promise.all([wslManager.listOnlineDistros(), wslManager.listDistros()])
.then(([onlineDistros, localDistros]) => {
const distroToInstall = onlineDistros.filter(d => !localDistros.some(l => l.name === d.name));
return distroToInstall.map(distroData => {
return {
...distroData,
label: `${distroData.friendlyName}`,
};
});
});
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
return picked;
}
export async function promptOpenRemoteWSLWindow(wslManager: WSLManager, useDefault: boolean, reuseWindow: boolean) {
let distroName: string | undefined;
if (useDefault) {
const distros = await wslManager.listDistros();
distroName = distros.find(distro => distro.isDefault)?.name;
} else {
distroName = (await showDistrosPicker(wslManager, 'Select WSL distro'))?.name;
}
if (!distroName) {
return;
}
openRemoteWSLWindow(distroName, reuseWindow);
}
export async function promptInstallNewWSLDistro(wslManager: WSLManager) {
const distroName = (await showOnlineDistrosPicker(wslManager, 'Select the WSL distro to install'))?.name;
if (!distroName) {
return;
}
wslTerminal.runCommand(`wsl.exe --install -d ${distroName}`);
}
export function openRemoteWSLWindow(distro: string, reuseWindow: boolean) {
vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: getRemoteAuthority(distro), reuseWindow });
}
export function openRemoteWSLLocationWindow(distro: string, path: string, reuseWindow: boolean) {
vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.from({ scheme: 'vscode-remote', authority: getRemoteAuthority(distro), path }), { forceNewWindow: !reuseWindow });
}
export async function setDefaultWSLDistro(wslManager: WSLManager, distroName: string) {
await wslManager.setDefaultDistro(distroName);
}
export async function deleteWSLDistro(wslManager: WSLManager, distroName: string) {
const deleteAction = 'Delete';
const resp = await vscode.window.showInformationMessage(`Are you sure you want to permanently delete the distro "${distroName}" including all its data?`, { modal: true }, deleteAction);
if (resp === deleteAction) {
await wslManager.deleteDistro(distroName);
return true;
}
return false;
}
@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function timeout(millis: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, millis));
}
export interface ITask<T> {
(): T;
}
export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i < retries; i++) {
try {
return await task();
} catch (error) {
lastError = error;
await timeout(delay);
}
}
throw lastError;
}
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export function disposeAll(disposables: vscode.Disposable[]): void {
while (disposables.length) {
const item = disposables.pop();
if (item) {
item.dispose();
}
}
}
export abstract class Disposable {
private _isDisposed = false;
protected _disposables: vscode.Disposable[] = [];
public dispose(): any {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
disposeAll(this._disposables);
}
protected _register<T extends vscode.Disposable>(value: T): T {
if (this._isDisposed) {
value.dispose();
} else {
this._disposables.push(value);
}
return value;
}
protected get isDisposed(): boolean {
return this._isDisposed;
}
}
@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface IDisposable {
dispose(): void;
}
export interface Event<T> {
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable;
}
/**
* Returns a promise that resolves when the event fires, or when cancellation
* is requested, whichever happens first.
*/
export function toPromise<T>(event: Event<T>): Promise<T>;
export function toPromise<T>(event: Event<T>, signal: AbortSignal): Promise<T | undefined>;
export function toPromise<T>(event: Event<T>, signal?: AbortSignal): Promise<T | undefined> {
if (!signal) {
return new Promise<T>((resolve) => once(event, resolve));
}
if (signal.aborted) {
return Promise.resolve(undefined);
}
return new Promise((resolve) => {
const d2 = once(event, (data) => {
(signal as any).removeEventListener('abort', d1);
resolve(data);
});
const d1 = () => {
d2.dispose();
(signal as any).removeEventListener('abort', d1);
resolve(undefined);
};
(signal as any).addEventListener('abort', d1);
});
}
/**
* Adds a handler that handles one event on the emitter, then disposes itself.
*/
export const once = <T>(event: Event<T>, listener: (data: T) => void): IDisposable => {
const disposable = event((value) => {
listener(value);
disposable.dispose();
});
return disposable;
};
/**
* Base event emitter. Calls listeners when data is emitted.
*/
export class EventEmitter<T> {
private listeners?: Array<(data: T) => void> | ((data: T) => void);
/**
* Event<T> function.
*/
public readonly event: Event<T> = (listener, thisArgs, disposables) => {
const d = this.add(thisArgs ? listener.bind(thisArgs) : listener);
disposables?.push(d);
return d;
};
/**
* Gets the number of event listeners.
*/
public get size() {
if (!this.listeners) {
return 0;
} else if (typeof this.listeners === 'function') {
return 1;
} else {
return this.listeners.length;
}
}
/**
* Emits event data.
*/
public fire(value: T) {
if (!this.listeners) {
// no-op
} else if (typeof this.listeners === 'function') {
this.listeners(value);
} else {
for (const listener of this.listeners) {
listener(value);
}
}
}
/**
* Disposes of the emitter.
*/
public dispose() {
this.listeners = undefined;
}
private add(listener: (data: T) => void): IDisposable {
if (!this.listeners) {
this.listeners = listener;
} else if (typeof this.listeners === 'function') {
this.listeners = [this.listeners, listener];
} else {
this.listeners.push(listener);
}
return { dispose: () => this.rm(listener) };
}
private rm(listener: (data: T) => void) {
if (!this.listeners) {
return;
}
if (typeof this.listeners === 'function') {
if (this.listeners === listener) {
this.listeners = undefined;
}
return;
}
const index = this.listeners.indexOf(listener);
if (index === -1) {
return;
}
if (this.listeners.length === 2) {
this.listeners = index === 0 ? this.listeners[1] : this.listeners[0];
} else {
this.listeners = this.listeners.slice(0, index).concat(this.listeners.slice(index + 1));
}
}
}
@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as os from 'os';
const homeDir = os.homedir();
export async function exists(path: string) {
try {
await fs.promises.access(path);
return true;
} catch {
return false;
}
}
export function untildify(path: string) {
return path.replace(/^~(?=$|\/|\\)/, homeDir);
}
export function normalizeToSlash(path: string) {
return path.replace(/\\/g, '/');
}
@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
type LogLevel = 'Trace' | 'Info' | 'Error';
export default class Log {
private output: vscode.OutputChannel;
constructor(name: string) {
this.output = vscode.window.createOutputChannel(name);
}
private data2String(data: any): string {
if (data instanceof Error) {
return data.stack || data.message;
}
if (data.success === false && data.message) {
return data.message;
}
return data.toString();
}
public trace(message: string, data?: any): void {
this.logLevel('Trace', message, data);
}
public info(message: string, data?: any): void {
this.logLevel('Info', message, data);
}
public error(message: string, data?: any): void {
this.logLevel('Error', message, data);
}
public logLevel(level: LogLevel, message: string, data?: any): void {
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
if (data) {
this.output.appendLine(this.data2String(data));
}
}
private now(): string {
const now = new Date();
return padLeft(now.getUTCHours() + '', 2, '0')
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
}
public show() {
this.output.show();
}
public dispose() {
this.output.dispose();
}
}
function padLeft(s: string, n: number, pad = ' ') {
return pad.repeat(Math.max(0, n - s.length)) + s;
}
@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const isWindows = process.platform === 'win32';
export const isMacintosh = process.platform === 'darwin';
export const isLinux = process.platform === 'linux';
@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as net from 'net';
/**
* Finds a random unused port assigned by the operating system. Will reject in case no free port can be found.
*/
export function findRandomPort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer({ pauseOnConnect: true });
server.on('error', reject);
server.on('listening', () => {
const port = (server.address() as net.AddressInfo).port;
server.close(() => resolve(port));
});
server.listen(0, '127.0.0.1');
});
}
/**
* Given a start point and a max number of retries, will find a port that
* is openable. Will return 0 in case no free port can be found.
*/
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
let done = false;
return new Promise(resolve => {
const timeoutHandle = setTimeout(() => {
if (!done) {
done = true;
return resolve(0);
}
}, timeout);
doFindFreePort(startPort, giveUpAfter, stride, (port) => {
if (!done) {
done = true;
clearTimeout(timeoutHandle);
return resolve(port);
}
});
});
}
function doFindFreePort(startPort: number, giveUpAfter: number, stride: number, clb: (port: number) => void): void {
if (giveUpAfter === 0) {
return clb(0);
}
const client = new net.Socket();
// If we can connect to the port it means the port is already taken so we continue searching
client.once('connect', () => {
dispose(client);
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
});
client.once('data', () => {
// this listener is required since node.js 8.x
});
client.once('error', (err: Error & { code?: string }) => {
dispose(client);
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
if (err.code !== 'ECONNREFUSED') {
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
}
// Otherwise it means the port is free to use!
return clb(startPort);
});
client.connect(startPort, '127.0.0.1');
}
/**
* Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener.
*/
export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
let resolved = false;
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
let countTried = 1;
const server = net.createServer({ pauseOnConnect: true });
function doResolve(port: number, resolve: (port: number) => void) {
if (!resolved) {
resolved = true;
server.removeAllListeners();
server.close();
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
resolve(port);
}
}
return new Promise<number>(resolve => {
timeoutHandle = setTimeout(() => {
doResolve(0, resolve);
}, timeout);
server.on('listening', () => {
doResolve(startPort, resolve);
});
server.on('error', err => {
if (err && ((<any>err).code === 'EADDRINUSE' || (<any>err).code === 'EACCES') && (countTried < giveUpAfter)) {
startPort++;
countTried++;
server.listen(startPort, '127.0.0.1');
} else {
doResolve(0, resolve);
}
});
server.on('close', () => {
doResolve(0, resolve);
});
server.listen(startPort, '127.0.0.1');
});
}
function dispose(socket: net.Socket): void {
try {
socket.removeAllListeners('connect');
socket.removeAllListeners('error');
socket.end();
socket.destroy();
socket.unref();
} catch (error) {
console.error(error); // otherwise this error would get lost in the callback chain
}
}
@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import { RemoteLocationHistory } from './remoteLocationHistory';
import { Disposable } from './common/disposable';
import { openRemoteWSLWindow, openRemoteWSLLocationWindow, promptInstallNewWSLDistro, deleteWSLDistro, setDefaultWSLDistro } from './commands';
import { WSLManager } from './wsl/wslManager';
class DistroItem {
constructor(
public name: string,
public isDefault: boolean,
public locations: string[]
) {
}
}
class DistroLocationItem {
constructor(
public path: string,
public name: string
) {
}
}
type DataTreeItem = DistroItem | DistroLocationItem;
export class DistroTreeDataProvider extends Disposable implements vscode.TreeDataProvider<DataTreeItem> {
private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter<DataTreeItem | DataTreeItem[] | void>());
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
constructor(
private readonly locationHistory: RemoteLocationHistory,
private readonly wslManager: WSLManager
) {
super();
this._register(vscode.commands.registerCommand('openremotewsl.explorer.addDistro', () => promptInstallNewWSLDistro(wslManager)));
this._register(vscode.commands.registerCommand('openremotewsl.explorer.refresh', () => this.refresh()));
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInNewWindow', e => this.openRemoteWSLWindow(e, false)));
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInCurrentWindow', e => this.openRemoteWSLWindow(e, true)));
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInNewWindow', e => this.openRemoteWSLocationWindow(e, false)));
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInCurrentWindow', e => this.openRemoteWSLocationWindow(e, true)));
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteFolderHistoryItem', e => this.deleteDistroLocation(e)));
this._register(vscode.commands.registerCommand('openremotewsl.explorer.setDefaultDistro', e => this.setDefaultDistro(e)));
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteDistro', e => this.deleteDistro(e)));
}
getTreeItem(element: DataTreeItem): vscode.TreeItem {
if (element instanceof DistroLocationItem) {
const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)');
const treeItem = new vscode.TreeItem(label);
treeItem.description = path.posix.dirname(element.path);
treeItem.iconPath = new vscode.ThemeIcon('folder');
treeItem.contextValue = 'openremotewsl.explorer.folder';
return treeItem;
}
const treeItem = new vscode.TreeItem(element.name);
treeItem.description = element.isDefault ? 'default distro' : undefined;
treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None;
treeItem.iconPath = new vscode.ThemeIcon('vm');
treeItem.contextValue = 'openremotewsl.explorer.distro';
return treeItem;
}
async getChildren(element?: DistroItem): Promise<DataTreeItem[]> {
if (!element) {
const distros = await this.wslManager.listDistros();
return distros.map(distro => new DistroItem(distro.name, distro.isDefault, this.locationHistory.getHistory(distro.name)));
}
if (element instanceof DistroItem) {
return element.locations.map(location => new DistroLocationItem(location, element.name));
}
return [];
}
private refresh() {
this._onDidChangeTreeData.fire();
}
private async deleteDistroLocation(element: DistroLocationItem) {
await this.locationHistory.removeLocation(element.name, element.path);
this.refresh();
}
private async openRemoteWSLWindow(element: DistroItem, reuseWindow: boolean) {
openRemoteWSLWindow(element.name, reuseWindow);
}
private async openRemoteWSLocationWindow(element: DistroLocationItem, reuseWindow: boolean) {
openRemoteWSLLocationWindow(element.name, element.path, reuseWindow);
}
private async setDefaultDistro(element: DistroItem) {
await setDefaultWSLDistro(this.wslManager, element.name);
this.refresh();
}
private async deleteDistro(element: DistroItem) {
await deleteWSLDistro(this.wslManager, element.name);
this.refresh();
}
}
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import Log from './common/logger';
import { RemoteWSLResolver, REMOTE_WSL_AUTHORITY } from './authResolver';
import { promptOpenRemoteWSLWindow } from './commands';
import { DistroTreeDataProvider } from './distroTreeView';
import { getRemoteWorkspaceLocationData, RemoteLocationHistory } from './remoteLocationHistory';
import { WSLManager } from './wsl/wslManager';
import { isWindows } from './common/platform';
export async function activate(context: vscode.ExtensionContext) {
if (!isWindows) {
return;
}
const logger = new Log('Remote - WSL');
context.subscriptions.push(logger);
const wslManager = new WSLManager(logger);
const remoteWSLResolver = new RemoteWSLResolver(wslManager, logger);
context.subscriptions.push(vscode.workspace.registerRemoteAuthorityResolver(REMOTE_WSL_AUTHORITY, remoteWSLResolver));
context.subscriptions.push(remoteWSLResolver);
const locationHistory = new RemoteLocationHistory(context);
const locationData = getRemoteWorkspaceLocationData();
if (locationData) {
await locationHistory.addLocation(locationData[0], locationData[1]);
}
const distroTreeDataProvider = new DistroTreeDataProvider(locationHistory, wslManager);
context.subscriptions.push(vscode.window.createTreeView('wslTargets', { treeDataProvider: distroTreeDataProvider }));
context.subscriptions.push(distroTreeDataProvider);
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connect', () => promptOpenRemoteWSLWindow(wslManager, true, true)));
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, true, false)));
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistro', () => promptOpenRemoteWSLWindow(wslManager, false, true)));
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistroInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, false, false)));
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.showLog', () => logger.show()));
}
export function deactivate() {
}
@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { REMOTE_WSL_AUTHORITY } from './authResolver';
export class RemoteLocationHistory {
private static STORAGE_KEY = 'remoteLocationHistory_v0';
private remoteLocationHistory: Record<string, string[]> = {};
constructor(private context: vscode.ExtensionContext) {
// context.globalState.update(RemoteLocationHistory.STORAGE_KEY, undefined);
this.remoteLocationHistory = context.globalState.get(RemoteLocationHistory.STORAGE_KEY) || {};
}
getHistory(host: string): string[] {
return this.remoteLocationHistory[host] || [];
}
async addLocation(host: string, path: string) {
const hostLocations = this.remoteLocationHistory[host] || [];
if (!hostLocations.includes(path)) {
hostLocations.unshift(path);
this.remoteLocationHistory[host] = hostLocations;
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
}
}
async removeLocation(host: string, path: string) {
let hostLocations = this.remoteLocationHistory[host] || [];
hostLocations = hostLocations.filter(l => l !== path);
this.remoteLocationHistory[host] = hostLocations;
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
}
}
export function getRemoteWorkspaceLocationData(): [string, string] | undefined {
let location = vscode.workspace.workspaceFile;
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY) && location.path.endsWith('.code-workspace')) {
const [, distroName] = location.authority.split('+');
return [distroName, location.path];
}
location = vscode.workspace.workspaceFolders?.[0].uri;
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY)) {
const [, distroName] = location.authority.split('+');
return [distroName, location.path];
}
return undefined;
}
@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
let vscodeProductJson: any;
async function getVSCodeProductJson() {
if (!vscodeProductJson) {
const productJsonStr = await fs.promises.readFile(path.join(vscode.env.appRoot, 'product.json'), 'utf8');
vscodeProductJson = JSON.parse(productJsonStr);
}
return vscodeProductJson;
}
export interface IServerConfig {
version: string;
commit: string;
quality: string;
release?: string; // void-like specific
serverApplicationName: string;
serverDataFolderName: string;
serverDownloadUrlTemplate?: string; // void-like specific
}
export async function getVSCodeServerConfig(): Promise<IServerConfig> {
const productJson = await getVSCodeProductJson();
return {
// version: vscode.version.replace('-insider', ''),
commit: productJson.commit,
quality: productJson.quality,
release: productJson.release,
serverApplicationName: productJson.serverApplicationName,
serverDataFolderName: productJson.serverDataFolderName,
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate,
// Void changed this
version: productJson.voidVersion
};
}
@@ -0,0 +1,353 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as crypto from 'crypto';
import Log from './common/logger';
import { getVSCodeServerConfig } from './serverConfig';
import { WSLManager } from './wsl/wslManager';
export interface ServerInstallOptions {
id: string;
quality: string;
commit: string;
version: string;
release?: string; // void specific
extensionIds: string[];
envVariables: string[];
serverApplicationName: string;
serverDataFolderName: string;
serverDownloadUrlTemplate: string;
}
export interface ServerInstallResult {
exitCode: number;
listeningOn: number;
connectionToken: string;
logFile: string;
osReleaseId: string;
arch: string;
platform: string;
tmpDir: string;
[key: string]: any;
}
export class ServerInstallError extends Error {
constructor(message: string) {
super(message);
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
const scriptId = crypto.randomBytes(12).toString('hex');
const vscodeServerConfig = await getVSCodeServerConfig();
const installOptions: ServerInstallOptions = {
id: scriptId,
version: vscodeServerConfig.version,
commit: vscodeServerConfig.commit,
quality: vscodeServerConfig.quality,
release: vscodeServerConfig.release,
extensionIds,
envVariables,
serverApplicationName: vscodeServerConfig.serverApplicationName,
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
};
const installServerScript = generateBashInstallScript(installOptions);
// Fish shell does not support heredoc so let's workaround it using -c option,
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
const resp = await wslManager.exec('bash', ['-c', `'${installServerScript.replace(/'/g, `'\\''`)}'`], distroName);
const endScriptRegex = new RegExp(`${scriptId}: Server installation script done`, 'm');
const commandOutput = await Promise.race([
resp.exitPromise.then(result => ({ stdout: resp.stdout, stderr: resp.stderr, exitCode: result.exitCode })),
new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => {
resp.onStdoutData(buffer => {
if (endScriptRegex.test(buffer.toString('utf8'))) {
resolve({ stdout: resp.stdout, stderr: resp.stderr, exitCode: 0 });
}
});
})
]);
if (commandOutput.exitCode) {
logger.trace('Server install command stderr:', commandOutput.stderr);
}
logger.trace('Server install command stdout:', commandOutput.stdout);
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
if (!resultMap) {
throw new ServerInstallError(`Failed parsing install script output`);
}
const exitCode = parseInt(resultMap.exitCode, 10);
if (exitCode !== 0) {
throw new ServerInstallError(`Couldn't install void server on remote server, install script returned non-zero exit status`);
}
const listeningOn = parseInt(resultMap.listeningOn, 10);
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
return {
exitCode,
listeningOn,
connectionToken: resultMap.connectionToken,
logFile: resultMap.logFile,
osReleaseId: resultMap.osReleaseId,
arch: resultMap.arch,
platform: resultMap.platform,
tmpDir: resultMap.tmpDir,
...remoteEnvVars
};
}
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
const startResultStr = `${scriptId}: start`;
const endResultStr = `${scriptId}: end`;
const startResultIdx = str.indexOf(startResultStr);
if (startResultIdx < 0) {
return undefined;
}
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
if (endResultIdx < 0) {
return undefined;
}
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
const resultMap: { [k: string]: string } = {};
const resultArr = installResult.split(/\r?\n/);
for (const line of resultArr) {
const [key, value] = line.split('==');
resultMap[key] = value;
}
return resultMap;
}
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
return `
# Server installation script
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
DISTRO_VERSION="${version}"
DISTRO_COMMIT="${commit}"
DISTRO_QUALITY="${quality}"
DISTRO_VSCODIUM_RELEASE="${release ?? ''}"
SERVER_APP_NAME="${serverApplicationName}"
SERVER_INITIAL_EXTENSIONS="${extensions}"
SERVER_LISTEN_FLAG="--port=0"
SERVER_DATA_DIR="$HOME/${serverDataFolderName}"
SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT"
SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME"
SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log"
SERVER_PIDFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.pid"
SERVER_TOKENFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.token"
SERVER_OS=
SERVER_ARCH=
SERVER_CONNECTION_TOKEN=
SERVER_DOWNLOAD_URL=
LISTENING_ON=
OS_RELEASE_ID=
ARCH=
PLATFORM=
# Mimic output from logs of remote-ssh extension
print_install_results_and_exit() {
echo "${id}: start"
echo "exitCode==$1=="
echo "listeningOn==$LISTENING_ON=="
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
echo "logFile==$SERVER_LOGFILE=="
echo "osReleaseId==$OS_RELEASE_ID=="
echo "arch==$ARCH=="
echo "platform==$PLATFORM=="
echo "tmpDir==$TMP_DIR=="
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
echo "${id}: end"
exit 0
}
# Check if platform is supported
PLATFORM="$(uname -s)"
case $PLATFORM in
Linux)
SERVER_OS="linux"
;;
*)
echo "Error platform not supported: $PLATFORM"
print_install_results_and_exit 1
;;
esac
# Check machine architecture
ARCH="$(uname -m)"
case $ARCH in
x86_64 | amd64)
SERVER_ARCH="x64"
;;
armv7l | armv8l)
SERVER_ARCH="armhf"
;;
arm64 | aarch64)
SERVER_ARCH="arm64"
;;
*)
echo "Error architecture not supported: $ARCH"
print_install_results_and_exit 1
;;
esac
# https://www.freedesktop.org/software/systemd/man/os-release.html
OS_RELEASE_ID="$(grep -i '^ID=' /etc/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
if [[ -z $OS_RELEASE_ID ]]; then
OS_RELEASE_ID="$(grep -i '^ID=' /usr/lib/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
if [[ -z $OS_RELEASE_ID ]]; then
OS_RELEASE_ID="unknown"
fi
fi
# Create installation folder
if [[ ! -d $SERVER_DIR ]]; then
mkdir -p $SERVER_DIR
if (( $? > 0 )); then
echo "Error creating server install directory"
print_install_results_and_exit 1
fi
fi
SERVER_DOWNLOAD_URL="$(echo "${serverDownloadUrlTemplate.replace(/\$\{/g, '\\${')}" | sed "s/\\\${quality}/$DISTRO_QUALITY/g" | sed "s/\\\${version}/$DISTRO_VERSION/g" | sed "s/\\\${commit}/$DISTRO_COMMIT/g" | sed "s/\\\${os}/$SERVER_OS/g" | sed "s/\\\${arch}/$SERVER_ARCH/g" | sed "s/\\\${release}/$DISTRO_VSCODIUM_RELEASE/g")"
# Check if server script is already installed
if [[ ! -f $SERVER_SCRIPT ]]; then
if [[ "$SERVER_OS" = "dragonfly" ]] || [[ "$SERVER_OS" = "freebsd" ]]; then
echo "Error "$SERVER_OS" needs manual installation of remote extension host"
print_install_results_and_exit 1
fi
pushd $SERVER_DIR > /dev/null
if [[ ! -z $(which wget) ]]; then
wget --tries=3 --timeout=10 --continue --no-verbose -O vscode-server.tar.gz $SERVER_DOWNLOAD_URL
elif [[ ! -z $(which curl) ]]; then
curl --retry 3 --connect-timeout 10 --location --show-error --silent --output vscode-server.tar.gz $SERVER_DOWNLOAD_URL
else
echo "Error no tool to download server binary"
print_install_results_and_exit 1
fi
if (( $? > 0 )); then
echo "Error downloading server from $SERVER_DOWNLOAD_URL"
print_install_results_and_exit 1
fi
tar -xf vscode-server.tar.gz --strip-components 1
if (( $? > 0 )); then
echo "Error while extracting server contents"
print_install_results_and_exit 1
fi
if [[ ! -f $SERVER_SCRIPT ]]; then
echo "Error server contents are corrupted"
print_install_results_and_exit 1
fi
rm -f vscode-server.tar.gz
popd > /dev/null
else
echo "Server script already installed in $SERVER_SCRIPT"
fi
# Try to find if server is already running
if [[ -f $SERVER_PIDFILE ]]; then
SERVER_PID="$(cat $SERVER_PIDFILE)"
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
else
SERVER_RUNNING_PROCESS="$(ps -o pid,args -A | grep $SERVER_SCRIPT | grep -v grep)"
fi
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
if [[ -f $SERVER_LOGFILE ]]; then
rm $SERVER_LOGFILE
fi
if [[ -f $SERVER_TOKENFILE ]]; then
rm $SERVER_TOKENFILE
fi
touch $SERVER_TOKENFILE
chmod 600 $SERVER_TOKENFILE
SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}"
echo $SERVER_CONNECTION_TOKEN > $SERVER_TOKENFILE
$SERVER_SCRIPT --start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --use-host-proxy --disable-websocket-compression --without-browser-env-var --enable-remote-auto-shutdown --accept-server-license-terms &> $SERVER_LOGFILE &
echo $! > $SERVER_PIDFILE
else
echo "Server script is already running $SERVER_SCRIPT"
fi
if [[ -f $SERVER_TOKENFILE ]]; then
SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)"
else
echo "Error server token file not found $SERVER_TOKENFILE"
print_install_results_and_exit 1
fi
if [[ -f $SERVER_LOGFILE ]]; then
for i in {1..5}; do
LISTENING_ON="$(cat $SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')"
if [[ -n $LISTENING_ON ]]; then
break
fi
sleep 0.5
done
if [[ -z $LISTENING_ON ]]; then
echo "Error server did not start sucessfully"
print_install_results_and_exit 1
fi
else
echo "Error server log file not found $SERVER_LOGFILE"
print_install_results_and_exit 1
fi
# Finish server setup and keep script running
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
echo "${id}: start"
echo "exitCode==0=="
echo "listeningOn==$LISTENING_ON=="
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
echo "logFile==$SERVER_LOGFILE=="
echo "osReleaseId==$OS_RELEASE_ID=="
echo "arch==$ARCH=="
echo "platform==$PLATFORM=="
echo "tmpDir==$TMP_DIR=="
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
echo "${id}: end"
echo "${id}: Server installation script done"
SERVER_PID="$(cat $SERVER_PIDFILE)"
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
while [[ -n $SERVER_RUNNING_PROCESS ]]; do
sleep 300;
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
done
else
print_install_results_and_exit 0
fi
`;
}
@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import Log from '../common/logger';
import { EventEmitter } from '../common/event';
const wslBinary = 'wsl.exe';
export interface WSLDistro {
isDefault: boolean;
name: string;
state: string;
version: string;
}
export interface WSLOnlineDistro {
name: string;
friendlyName: string;
}
export class WSLManager {
constructor(private readonly logger: Log) {
}
async listDistros() {
const resp = this._runWSLCommand(['--list', '--verbose'], 'utf16le');
const { exitCode } = await resp.exitPromise;
const { stdout, stderr } = resp;
if (exitCode) {
this.logger.trace(`Command wsl listDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
throw new Error(`Command wsl listDistros exited with code ${exitCode}`);
}
const regex = /(?<default>\*|\s)\s+(?<name>[\w\.-]+)\s+(?<state>[\w]+)\s+(?<version>\d)/;
const distros: WSLDistro[] = [];
for (const line of stdout.split(/\r?\n/)) {
const matches = line.match(regex);
if (matches && matches.groups) {
distros.push({
isDefault: matches.groups.default === '*',
name: matches.groups.name,
state: matches.groups.state,
version: matches.groups.version,
});
}
}
return distros;
}
async listOnlineDistros() {
const resp = this._runWSLCommand(['--list', '--online'], 'utf16le');
const { exitCode } = await resp.exitPromise;
const { stdout, stderr } = resp;
if (exitCode) {
this.logger.trace(`Command wsl listOnlineDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
throw new Error(`Command wsl listOnlineDistros exited with code ${exitCode}`);
}
let lines = stdout.split(/\r?\n/);
const idx = lines.findIndex(l => /\s*NAME\s+FRIENDLY NAME\s*/.test(l));
lines = lines.slice(idx + 1);
const regex = /(?<name>[\w\.-]+)\s+(?<friendlyName>\w.+\w)/;
const distros: WSLOnlineDistro[] = [];
for (const line of lines) {
const matches = line.match(regex);
if (matches && matches.groups) {
distros.push({
name: matches.groups.name,
friendlyName: matches.groups.friendlyName,
});
}
}
return distros;
}
async setDefaultDistro(distroName: string) {
const resp = this._runWSLCommand(['--set-default', distroName], 'utf16le');
const { exitCode } = await resp.exitPromise;
const { stdout, stderr } = resp;
if (exitCode) {
this.logger.trace(`Command wsl setDefaultDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
throw new Error(`Command wsl setDefaultDistro exited with code ${exitCode}`);
}
}
async deleteDistro(distroName: string) {
const resp = this._runWSLCommand(['--unregister', distroName], 'utf16le');
const { exitCode } = await resp.exitPromise;
const { stdout, stderr } = resp;
if (exitCode) {
this.logger.trace(`Command wsl deleteDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
throw new Error(`Command wsl deleteDistro exited with code ${exitCode}`);
}
}
async exec(cmd: string, args: string[], distro: string) {
return this._runWSLCommand(['--distribution', distro, '--', cmd, ...args], 'utf8');
}
private _runWSLCommand(args: string[], encoding: 'utf8' | 'utf16le') {
this.logger.trace(`Running WSL command: ${wslBinary} ${args.join(' ')}`);
const cmd = cp.spawn(wslBinary, args, { windowsHide: true, windowsVerbatimArguments: true });
const stdoutDataEmitter = new EventEmitter<Buffer>();
const stdoutData: Buffer[] = [];
const stderrDataEmitter = new EventEmitter<Buffer>();
const stderrData: Buffer[] = [];
cmd.stdout.on('data', (data: Buffer) => {
stdoutData.push(data);
stdoutDataEmitter.fire(data);
});
cmd.stderr.on('data', (data: Buffer) => {
stderrData.push(data);
stderrDataEmitter.fire(data);
});
const exitPromise = new Promise<{ exitCode: number }>((resolve, reject) => {
cmd.on('error', (err) => {
this.logger.error(`Error running WSL command: ${wslBinary} ${args.join(' ')}`, err);
reject(err);
});
cmd.on('exit', (code, _signal) => {
resolve({ exitCode: code ?? 0 });
});
});
return {
get stdout() {
return Buffer.concat(stdoutData).toString(encoding);
},
get stderr() {
return Buffer.concat(stderrData).toString(encoding);
},
get onStdoutData() {
return stdoutDataEmitter.event;
},
get onStderrData() {
return stderrDataEmitter.event;
},
exitPromise
};
}
}
@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
class WSLTerminal {
static NAME = 'WSL';
private getTerminal() {
const wslTerminal = vscode.window.terminals.find(t => t.name === WSLTerminal.NAME);
if (wslTerminal) {
return wslTerminal;
}
return vscode.window.createTerminal(WSLTerminal.NAME);
}
runCommand(command: string) {
const wslTerminal = this.getTerminal();
wslTerminal.show(false);
wslTerminal.sendText(command, true);
}
}
export default new WSLTerminal();
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out",
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.resolvers.d.ts",
"../../src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts",
]
}
+6418 -3945
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -77,6 +77,7 @@
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.9.0",
"@parcel/watcher": "2.5.1",
"@types/semver": "^7.5.8",
"@vscode/deviceid": "^0.1.1",
@@ -102,6 +103,7 @@
"@xterm/addon-webgl": "^0.19.0-beta.98",
"@xterm/headless": "^5.6.0-beta.98",
"@xterm/xterm": "^5.6.0-beta.98",
"ajv": "^8.17.1",
"cross-spawn": "^7.0.6",
"diff": "^7.0.0",
"eslint-plugin-react": "^7.37.4",
@@ -109,6 +111,7 @@
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.4",
"katex": "^0.16.22",
"kerberos": "2.1.1",
"lucide-react": "^0.477.0",
"marked": "^15.0.7",
@@ -123,6 +126,7 @@
"posthog-node": "^4.8.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-tooltip": "^5.28.1",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-html-languageservice": "^5.3.1",
@@ -143,6 +147,7 @@
"@types/eslint": "^9.6.1",
"@types/gulp-svgmin": "^1.2.1",
"@types/http-proxy-agent": "^2.0.1",
"@types/katex": "^0.16.7",
"@types/kerberos": "^1.1.2",
"@types/minimist": "^1.2.1",
"@types/mocha": "^9.1.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.0.3",
"voidVersion": "1.2.6",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
@@ -34,11 +34,8 @@ mkdir -pv "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
# Exclude create-appimage.sh and appimagetool-x86_64.AppImage from being copied
echo "Copying files excluding create-appimage.sh and appimagetool-x86_64.AppImage..."
for file in ./*; do
if [[ "$file" != "./create-appimage.sh" && "$file" != "./appimagetool-x86_64.AppImage" ]]; then
cp -rv "$file" "$APP_DIR/usr/bin/"
fi
done
cp -v ./void "$APP_DIR/usr/bin/"
# Copy the icon to required locations
cp -v ./void.png "$APP_DIR/void.png"
+13
View File
@@ -1,3 +1,16 @@
# README
This is a community-made AppImage creation script.
There are some reported bugs with it.
To generate an AppImage yourself, feel free to look at
stable-linux.yml in the separate `void-builder/` repo,
which runs a GitHub Action that builds the AppImage you see on our website.
# Void AppImage Creation Script
This script automates the process of creating an AppImage for the Void Editor using Docker. It works on macOS and Linux platforms.
+3
View File
@@ -3,6 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This bootstrap-fork module handles the initialization of a forked process in VS Code.
// It sets up logging, exception handling, and loads the ESM module system.
import * as performance from './vs/base/common/performance.js';
import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from './bootstrap-node.js';
import { bootstrapESM } from './bootstrap-esm.js';
@@ -121,6 +121,93 @@ export class SmartSelectController implements IEditorContribution {
}
this._state = this._state.map(state => state.mov(forward));
const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition()));
// Void changed this to skip over added whitespace when using smartSelect
// // Store the original selections for comparison
// const originalSelections = selections;
// // Keep skipping while we're only adding/removing whitespace
// let keepSkipping = true;
// let skipCount = 0;
// const MAX_SKIPS = 5; // Avoid infinite loops by setting a reasonable limit
// while (keepSkipping && skipCount < MAX_SKIPS) {
// keepSkipping = false; // Reset for each iteration
// // Check if all selections only added/removed whitespace
// if (originalSelections.length === newSelections.length) {
// for (let i = 0; i < originalSelections.length; i++) {
// const oldSel = originalSelections[i];
// const newSel = newSelections[i];
// if (forward) { // For expanding (^+Shift+Right)
// // Skip if only whitespace was added
// const oldText = model.getValueInRange(oldSel).trim();
// const newText = model.getValueInRange(newSel).trim();
// const onlyWhitespaceAdded = oldText === newText && oldText.length > 0;
// if (onlyWhitespaceAdded) {
// console.log(`SMART SELECT - SKIPPING (EXPAND) [${skipCount + 1}]:`, {
// reason: 'only whitespace added',
// oldText: model.getValueInRange(oldSel),
// newText: model.getValueInRange(newSel)
// });
// keepSkipping = true;
// break;
// }
// } else { // For shrinking (^+Shift+Left)
// // Skip if only whitespace was removed
// const oldText = model.getValueInRange(oldSel).trim();
// const newText = model.getValueInRange(newSel).trim();
// const onlyWhitespaceRemoved = oldText === newText && newText.length > 0;
// if (onlyWhitespaceRemoved) {
// console.log(`SMART SELECT - SKIPPING (SHRINK) [${skipCount + 1}]:`, {
// reason: 'only whitespace removed',
// oldText: model.getValueInRange(oldSel),
// newText: model.getValueInRange(newSel)
// });
// keepSkipping = true;
// break;
// }
// }
// }
// }
// // If we need to skip, move one more time
// if (keepSkipping) {
// skipCount++;
// // Try to move to the next range
// const prevState = this._state;
// this._state = this._state.map(state => state.mov(forward));
// // Check if we've reached the end of available ranges
// const stateUnchanged = this._state.every((state, idx) =>
// state.index === prevState[idx].index
// );
// if (stateUnchanged) {
// // We can't move any further, so stop skipping
// keepSkipping = false;
// } else {
// // Update selections for the next iteration
// newSelections = this._state.map(state => Selection.fromPositions(
// state.ranges[state.index].getStartPosition(),
// state.ranges[state.index].getEndPosition()
// ));
// }
// }
// }
// // Print AFTER selection (before actually setting it)
// console.log('SMART SELECT - AFTER:', newSelections.map(s => {
// return {
// range: `(${s.startLineNumber},${s.startColumn}) -> (${s.endLineNumber},${s.endColumn})`,
// text: model.getValueInRange(s)
// };
// }));
this._ignoreSelection = true;
try {
this._editor.setSelections(newSelections);
@@ -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;
@@ -22,10 +22,12 @@ import { ColorScheme } from '../../web.api.js';
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
import { splitRecentLabel } from '../../../../base/common/labels.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
/* eslint-disable */ // Void
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
import { VIEWLET_ID as REMOTE_EXPLORER_VIEWLET_ID } from '../../../contrib/remote/browser/remoteExplorer.js';
/* eslint-enable */
// interface WatermarkEntry {
@@ -98,6 +100,7 @@ export class EditorGroupWatermark extends Disposable {
@ICommandService private readonly commandService: ICommandService,
@IHostService private readonly hostService: IHostService,
@ILabelService private readonly labelService: ILabelService,
@IViewsService private readonly viewsService: IViewsService,
) {
super();
@@ -182,14 +185,21 @@ export class EditorGroupWatermark extends Disposable {
// Void - if the workbench is empty, show open
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
// Create a flex container for buttons with vertical direction
const buttonContainer = $('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.flexDirection = 'column'; // Change to column for vertical stacking
buttonContainer.style.alignItems = 'center'; // Center the buttons horizontally
buttonContainer.style.gap = '8px'; // Reduce gap between buttons from 16px to 8px
buttonContainer.style.marginBottom = '16px';
voidIconBox.appendChild(buttonContainer);
// Open a folder
const openFolderButton = h('button')
openFolderButton.root.classList.add('void-watermark-button')
openFolderButton.root.classList.add('void-openfolder-button')
openFolderButton.root.style.display = 'block'
openFolderButton.root.style.marginLeft = 'auto'
openFolderButton.root.style.marginRight = 'auto'
openFolderButton.root.style.marginBottom = '16px'
openFolderButton.root.textContent = 'Open a folder'
openFolderButton.root.style.width = '124px' // Set width to 124px as requested
openFolderButton.root.textContent = 'Open Folder'
openFolderButton.root.onclick = () => {
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
@@ -198,7 +208,19 @@ export class EditorGroupWatermark extends Disposable {
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
// }
}
voidIconBox.appendChild(openFolderButton.root);
buttonContainer.appendChild(openFolderButton.root);
// Open SSH button
const openSSHButton = h('button')
openSSHButton.root.classList.add('void-openssh-button')
openSSHButton.root.style.display = 'block'
openSSHButton.root.style.backgroundColor = '#5a5a5a' // Made darker than the default gray
openSSHButton.root.style.width = '124px' // Set width to 124px as requested
openSSHButton.root.textContent = 'Open SSH'
openSSHButton.root.onclick = () => {
this.viewsService.openViewContainer(REMOTE_EXPLORER_VIEWLET_ID);
}
buttonContainer.appendChild(openSSHButton.root);
// Recents
@@ -244,6 +266,9 @@ export class EditorGroupWatermark extends Disposable {
const dirSpan = $('span');
dirSpan.style.paddingLeft = '4px';
dirSpan.style.whiteSpace = 'nowrap';
dirSpan.style.overflow = 'hidden';
dirSpan.style.maxWidth = '300px';
dirSpan.innerText = parentPath;
dirSpan.title = fullPath;
@@ -50,6 +50,7 @@
}
/* light */
.void-void-icon,
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
width: 100%;
max-height: 100%;
@@ -60,14 +61,17 @@
background-repeat: no-repeat;
}
.void-void-icon,
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./void_cube_noshadow.png'); /* // Void */
}
.void-void-icon,
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./void_cube_noshadow.png'); /* // Void */
}
.void-void-icon,
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./void_cube_noshadow.png'); /* // Void */
}
@@ -612,7 +612,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
// On Mac, the delay is 1500.
'default': isMacintosh ? 1500 : 500,
'default': 300, // Void changed this from isMacintosh ? 1500 : 500,
'minimum': 0
},
'workbench.reduceMotion': {
@@ -648,15 +648,18 @@ const defaultChat = {
providerSetting: product.defaultChatAgent?.providerSetting ?? '',
};
// Void commented this out - copilot head
// Add next to the command center if command center is disabled
MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
/* MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
submenu: MenuId.ChatTitleBarMenu,
title: localize('title4', "Copilot"),
icon: Codicon.copilot,
when: ContextKeyExpr.and(
ChatContextKeys.supported,
ContextKeyExpr.has('config.chat.commandCenter.enabled')
),
// Void commented this out - copilot head
when: ContextKeyExpr.false(),
// when: ContextKeyExpr.and(
// ChatContextKeys.supported,
// ContextKeyExpr.has('config.chat.commandCenter.enabled')
// ),
order: 10001 // to the right of command center
});
@@ -666,13 +669,15 @@ MenuRegistry.appendMenuItem(MenuId.TitleBar, {
title: localize('title4', "Copilot"),
group: 'navigation',
icon: Codicon.copilot,
when: ContextKeyExpr.and(
ChatContextKeys.supported,
ContextKeyExpr.has('config.chat.commandCenter.enabled'),
ContextKeyExpr.has('config.window.commandCenter').negate(),
),
when: ContextKeyExpr.false(),
// Void commented this out - copilot head
// when: ContextKeyExpr.and(
// ChatContextKeys.supported,
// ContextKeyExpr.has('config.chat.commandCenter.enabled'),
// ContextKeyExpr.has('config.window.commandCenter').negate(),
// ),
order: 1
});
}); */
registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction {
constructor() {
@@ -0,0 +1,63 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
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';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
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; // 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({
f1: true,
id: 'void.dummy',
title: localize2('dummy', 'dummy: Init'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.Digit0,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const n = accessor.get(IDummyService)
console.log('Hi', n._serviceBrand)
}
})
class DummyService extends Disposable implements IWorkbenchContribution, IDummyService {
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); // lazily loaded, even if Eager
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); // mounts on start
@@ -34,7 +34,7 @@
// // const result = await new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['grep_search'],
// // tools: ['search_for_files'],
// // onFinalMessage: ({ result: r, }) => {
// // res(r)
// // },
@@ -73,7 +73,7 @@
// // const result = new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['grep_search'],
// // tools: ['search_for_files'],
// // onResult: (r) => {
// // res(r)
// // }
@@ -8,8 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { Position } from '../../../../editor/common/core/position.js';
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { InlineCompletion, } from '../../../../editor/common/languages.js';
import { Range } from '../../../../editor/common/core/range.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
@@ -21,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';
@@ -633,8 +633,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
async _provideInlineCompletionItems(
model: ITextModel,
position: Position,
context: InlineCompletionContext,
token: CancellationToken,
): Promise<InlineCompletion[]> {
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
@@ -793,19 +791,20 @@ 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[modelSelection.providerName]?.[modelSelection.modelName] : undefined
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
// 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,
}
}),
modelSelection,
modelSelectionOptions,
logging: { loggingName: 'Autocomplete' },
@@ -852,7 +851,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(message)
},
onAbort: () => { },
onAbort: () => { reject('Aborted autocomplete') },
})
newAutocompletion.requestId = requestId
@@ -893,13 +892,14 @@ 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()
this._langFeatureService.inlineCompletionsProvider.register('*', {
this._register(this._langFeatureService.inlineCompletionsProvider.register('*', {
provideInlineCompletions: async (model, position, context, token) => {
const items = await this._provideInlineCompletionItems(model, position, context, token)
const items = await this._provideInlineCompletionItems(model, position)
// console.log('item: ', items?.[0]?.insertText)
return { items: items, }
@@ -936,7 +936,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
});
},
})
}))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,648 @@
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 { reParsedToolXMLString, 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';
import { IVoidModelService } from '../common/voidModelService.js';
import { URI } from '../../../../base/common/uri.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
}
}
else {
newMessages[i] = {
role: 'assistant',
content: currMsg.content,
// strip away 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${reParsedToolXMLString(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, }): { 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,
@IVoidModelService private readonly voidModelService: IVoidModelService,
) {
super()
}
// Read .voidrules files from workspace folders
private _getVoidRulesFileContents(): string {
try {
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
let voidRules = '';
for (const folder of workspaceFolders) {
const uri = URI.joinPath(folder.uri, '.voidrules')
const { model } = this.voidModelService.getModel(uri)
if (!model) continue
voidRules += model.getValue() + '\n\n';
}
return voidRules.trim();
}
catch (e) {
return ''
}
}
// Get combined AI instructions from settings and .voidrules files
private _getCombinedAIInstructions(): string {
const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions;
const voidRulesFileContent = this._getVoidRulesFileContents();
const ans: string[] = []
if (globalAIInstructions) ans.push(globalAIInstructions)
if (voidRulesFileContent) ans.push(voidRulesFileContent)
return ans.join('\n\n')
}
// 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
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]
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
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]
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
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 }) => {
// Get combined AI instructions with the provided aiInstructions as the base
const combinedInstructions = this._getCombinedAIInstructions();
let prefix = `\
${!combinedInstructions ? '' : `\
// Instructions:
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
${combinedInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
${messages.prefix}`
const suffix = messages.suffix
const stopTokens = messages.stopTokens
return { prefix, suffix, stopTokens }
}
}
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"
}
}
}
*/
@@ -0,0 +1,37 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { IVoidModelService } from '../common/voidModelService.js';
class ConvertContribWorkbenchContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.void.convertcontrib'
_serviceBrand: undefined;
constructor(
@IVoidModelService private readonly voidModelService: IVoidModelService,
@IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService,
) {
super()
const initializeURI = (uri: URI) => {
this.workspaceContext.getWorkspace()
const voidRulesURI = URI.joinPath(uri, '.voidrules')
this.voidModelService.initializeModel(voidRulesURI)
}
// call
this._register(this.workspaceContext.onDidChangeWorkspaceFolders((e) => {
[...e.changed, ...e.added].forEach(w => { initializeURI(w.uri) })
}))
this.workspaceContext.getWorkspace().folders.forEach(w => { initializeURI(w.uri) })
}
}
registerWorkbenchContribution2(ConvertContribWorkbenchContribution.ID, ConvertContribWorkbenchContribution, WorkbenchPhase.BlockRestore);
@@ -0,0 +1,428 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../base/common/uri.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
import { IExplorerService } from '../../files/browser/files.js';
import { SortOrder } from '../../files/common/files.js';
import { ExplorerItem } from '../../files/common/explorerModel.js';
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
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, options?: { maxItemsPerDir?: number }): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
}
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
// Check if it's a known filtered type like .git
const shouldExcludeDirectory = (item: ExplorerItem) => {
if (item.name === '.git' ||
item.name === 'node_modules' ||
item.name.startsWith('.') ||
item.name === 'dist' ||
item.name === 'build' ||
item.name === 'out' ||
item.name === 'bin' ||
item.name === 'coverage' ||
item.name === '__pycache__' ||
item.name === 'env' ||
item.name === 'venv' ||
item.name === 'tmp' ||
item.name === 'temp' ||
item.name === 'artifacts' ||
item.name === 'target' ||
item.name === 'obj' ||
item.name === 'vendor' ||
item.name === 'logs' ||
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;
}
// ---------- ONE LAYER DEEP ----------
export const computeDirectoryTree1Deep = async (
fileService: IFileService,
rootURI: URI,
pageNumber: number = 1,
): Promise<ToolResultType['ls_dir']> => {
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
if (!stat.isDirectory) {
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
}
const nChildren = stat.children?.length ?? 0;
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1);
const children: ShallowDirectoryItem[] = listChildren?.map(child => ({
name: child.name,
uri: child.resource,
isDirectory: child.isDirectory,
isSymbolicLink: child.isSymbolicLink
})) ?? [];
const hasNextPage = (nChildren - 1) > toChildIdx;
const hasPrevPage = pageNumber > 1;
const itemsRemaining = Math.max(0, nChildren - (toChildIdx + 1));
return {
children,
hasNextPage,
hasPrevPage,
itemsRemaining
};
};
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
if (!result.children) {
return `Error: ${params.uri} is not a directory`;
}
let output = '';
const entries = result.children;
if (!result.hasPrevPage) { // is first page
output += `${params.uri.fsPath}\n`;
}
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const isLast = i === entries.length - 1 && !result.hasNextPage;
const prefix = isLast ? '└── ' : '├── ';
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
}
if (result.hasNextPage) {
output += `└── (${result.itemsRemaining} results remaining...)\n`;
}
return output;
};
// ---------- IN GENERAL ----------
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
const computeAndStringifyDirectoryTree = async (
eItem: ExplorerItem,
explorerService: IExplorerService,
MAX_CHARS: number,
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 };
}
// Increment file count
fileCount.count++;
// Add the root node first (without tree characters)
const nodeLine = `${eItem.name}${eItem.isDirectory ? '/' : ''}${eItem.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
if (nodeLine.length > MAX_CHARS) {
return { content: '', wasCutOff: true };
}
let content = nodeLine;
let wasCutOff = false;
let remainingChars = MAX_CHARS - nodeLine.length;
// 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 renderChildrenCombined = async (
children: ExplorerItem[],
maxChars: number,
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;
// 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 (childLine.length > remainingChars) {
childrenCutOff = true;
break;
}
childrenContent += childLine;
remainingChars -= childLine.length;
fileCount.count++;
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
// Skip processing children for git ignored directories
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
// Create the prefix for the next level (continuation line or space)
if (child.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await child.fetchChildren(SortOrder.Modified);
if (eChildren && eChildren.length > 0) {
const {
childrenContent: grandChildrenContent,
childrenCutOff: grandChildrenCutOff
} = await renderChildrenCombined(
eChildren,
remainingChars,
nextLevelPrefix,
explorerService,
fileCount,
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
);
if (grandChildrenContent.length > 0) {
childrenContent += grandChildrenContent;
remainingChars -= grandChildrenContent.length;
}
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 };
};
// ---------------------------------------------------
class DirectoryStrService extends Disposable implements IDirectoryStrService {
_serviceBrand: undefined;
constructor(
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IExplorerService private readonly explorerService: IExplorerService,
) {
super();
}
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
const eRoot = this.explorerService.findClosest(uri)
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
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}`
if (wasCutOff) c = `${c}\n...Result was truncated...`
return c
}
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';
// this prioritizes filling 1st workspace before any other, etc
const f = folders[i];
str += `Directory of ${f.uri.fsPath}:\n`;
const rootURI = f.uri;
const eRoot = this.explorerService.findClosestRoot(rootURI);
if (!eRoot) continue;
// First try with START_MAX_DEPTH and startMaxItemsPerDir
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
);
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
let content, wasCutOff;
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
);
content = result.content;
wasCutOff = result.wasCutOff;
} else {
content = initialContent;
wasCutOff = initialCutOff;
}
str += content;
if (wasCutOff) {
cutOff = true;
break;
}
}
if (cutOff) {
return `${str.trimEnd()}\n${cutOffMessage}`
}
return str
}
}
registerSingleton(IDirectoryStrService, DirectoryStrService, InstantiationType.Delayed);
@@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
// import { throttle } from '../../../../base/common/decorators.js';
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
import { findDiffs } from './helpers/findDiffs.js';
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
@@ -31,22 +31,22 @@ import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
import { QuickEditPropsType } from './quickEditActions.js';
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
import { isMacintosh } from '../../../../base/common/platform.js';
import { INotificationService, } from '../../../../platform/notification/common/notification.js';
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
import { Emitter } from '../../../../base/common/event.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js';
import { IMetricsService } from '../common/metricsService.js';
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
// import { isMacintosh } from '../../../../base/common/platform.js';
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@@ -71,6 +71,21 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
const numLinesOfStr = (str: string) => str.split('\n').length
export const getLengthOfTextPx = ({ tabWidth, spaceWidth, content }: { tabWidth: number, spaceWidth: number, content: string }) => {
let lengthOfTextPx = 0;
for (const char of content) {
if (char === '\t') {
lengthOfTextPx += tabWidth
} else {
lengthOfTextPx += spaceWidth;
}
}
return lengthOfTextPx
}
const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
const model = editor.getModel();
@@ -94,28 +109,42 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth;
const tabWidth = numSpacesInTab * spaceWidth;
let paddingLeft = 0;
for (const char of leadingWhitespace) {
if (char === '\t') {
paddingLeft += tabWidth
} else if (char === ' ') {
paddingLeft += spaceWidth;
}
}
const leftWhitespacePx = getLengthOfTextPx({
tabWidth,
spaceWidth,
content: leadingWhitespace
});
return paddingLeft;
return leftWhitespacePx;
};
// Helper function to remove whitespace except newlines
const removeWhitespaceExceptNewlines = (str: string): string => {
return str.replace(/[^\S\n]+/g, '');
}
// finds block.orig in fileContents and return its range in file
// startingAtLine is 1-indexed and inclusive
const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => {
const idx = fileContents.indexOf(text,
startingAtLine !== undefined ?
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
: 0
)
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => {
const startLineIdx = (fileContents: string) => startingAtLine !== undefined ?
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
: 0
// idx = starting index in fileContents
let idx = fileContents.indexOf(text, startLineIdx(fileContents))
// try to find it ignoring all whitespace this time
if (idx === -1 && canFallbackToRemoveWhitespace) {
text = removeWhitespaceExceptNewlines(text)
fileContents = removeWhitespaceExceptNewlines(fileContents)
idx = fileContents.indexOf(text, startLineIdx(fileContents));
}
if (idx === -1) return 'Not found' as const
const lastIdx = fileContents.lastIndexOf(text)
if (lastIdx !== idx) return 'Not unique' as const
@@ -127,108 +156,6 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num
// // TODO diffArea should be removed if we just discovered it has no more diffs in it
// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
// const diffArea = this.diffAreaOfId[diffareaid]
// if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) {
// const { onFinishEdit } = this._addToHistory(uri)
// this._deleteDiffArea(diffArea)
// onFinishEdit()
// }
// }
export type Diff = {
diffid: number;
diffareaid: number; // the diff area this diff belongs to, "computed"
} & ComputedDiff
// _ means anything we don't include if we clone it
// DiffArea.originalStartLine is the line in originalCode (not the file)
type CommonZoneProps = {
diffareaid: number;
startLine: number;
endLine: number;
_URI: URI; // typically we get the URI from model
}
type CtrlKZone = {
type: 'CtrlKZone';
originalCode?: undefined;
editorId: string; // the editor the input lives on
_mountInfo: null | {
textAreaRef: { current: HTMLTextAreaElement | null }
dispose: () => void;
refresh: () => void;
}
_linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
} & CommonZoneProps
export type DiffZone = {
type: 'DiffZone',
originalCode: string;
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
_streamState: {
isStreaming: true;
streamRequestIdRef: { current: string | null };
line: number;
} | {
isStreaming: false;
streamRequestIdRef?: undefined;
line?: undefined;
};
editorId?: undefined;
linkedStreamingDiffZone?: undefined;
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
} & CommonZoneProps
type TrackingZone<T> = {
type: 'TrackingZone';
metadata: T;
originalCode?: undefined;
editorId?: undefined;
_removeStylesFns?: undefined;
} & CommonZoneProps
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
const diffAreaSnapshotKeys = [
'type',
'diffareaid',
'originalCode',
'startLine',
'endLine',
'editorId',
] as const satisfies (keyof DiffArea)[]
type DiffAreaSnapshot<DiffAreaType extends DiffArea = DiffArea> = Pick<DiffAreaType, typeof diffAreaSnapshotKeys[number]>
type HistorySnapshot = {
snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot>;
entireFileCode: string;
}
// line/col is the location, originalCodeStartLine is the start line of the original code being displayed
type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }
@@ -243,23 +170,21 @@ class EditCodeService extends Disposable implements IEditCodeService {
diffAreaOfId: Record<string, DiffArea> = {}; // diffareaId -> diffArea
diffOfId: Record<string, Diff> = {}; // diffid -> diff (redundant with diffArea._diffOfId)
// events
// uri: diffZones // listen on change diffZones
private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>();
onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event;
// diffZone: [uri], diffs, isStreaming // listen on change diffs, change streaming (uri is const)
private readonly _onDidChangeDiffsInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
private readonly _onDidChangeDiffsInDiffZoneNotStreaming = new Emitter<{ uri: URI, diffareaid: number }>();
private readonly _onDidChangeStreamingInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
onDidChangeDiffsInDiffZone = this._onDidChangeDiffsInDiffZone.event;
onDidChangeDiffsInDiffZoneNotStreaming = this._onDidChangeDiffsInDiffZoneNotStreaming.event;
onDidChangeStreamingInDiffZone = this._onDidChangeStreamingInDiffZone.event;
// ctrlKZone: [uri], isStreaming // listen on change streaming
private readonly _onDidChangeStreamingInCtrlKZone = new Emitter<{ uri: URI; diffareaid: number }>();
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event;
constructor(
@@ -273,11 +198,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
@IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService,
@IMetricsService private readonly _metricsService: IMetricsService,
@INotificationService private readonly _notificationService: INotificationService,
@ICommandService private readonly _commandService: ICommandService,
// @ICommandService private readonly _commandService: ICommandService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IFileService private readonly _fileService: IFileService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
@ITextFileService private readonly _textFileService: ITextFileService,
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
) {
super();
@@ -332,29 +257,45 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._realignAllDiffAreasLines(uri, change.text, change.range)
}
this._refreshStylesAndDiffsInURI(uri)
// if diffarea has no diffs after a user edit, delete it
const diffAreasToDelete: DiffZone[] = []
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] ?? []) {
const diffArea = this.diffAreaOfId[diffareaid] ?? null
const shouldDelete = diffArea?.type === 'DiffZone' && Object.keys(diffArea._diffOfId).length === 0
if (shouldDelete) {
diffAreasToDelete.push(diffArea)
}
}
if (diffAreasToDelete.length !== 0) {
const { onFinishEdit } = this._addToHistory(uri)
diffAreasToDelete.forEach(da => this._deleteDiffZone(da))
onFinishEdit()
}
}
private _notifyError = (e: Parameters<OnError>[0]) => {
const details = errorDetails(e.fullError)
this._notificationService.notify({
severity: Severity.Warning,
message: `Void Error: ${e.message}`,
actions: {
secondary: [{
id: 'void.onerror.opensettings',
enabled: true,
label: `Open Void's settings`,
tooltip: '',
class: undefined,
run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
}]
},
source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
})
}
// private _notifyError = (e: Parameters<OnError>[0]) => {
// const details = errorDetails(e.fullError)
// this._notificationService.notify({
// severity: Severity.Warning,
// message: `Void Error: ${e.message}`,
// actions: {
// secondary: [{
// id: 'void.onerror.opensettings',
// enabled: true,
// label: `Open Void's settings`,
// tooltip: '',
// class: undefined,
// run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
// }]
// },
// source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
// })
// }
@@ -722,106 +663,114 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _getCurrentVoidFileSnapshot = (uri: URI): VoidFileSnapshot => {
const { model } = this._voidModelService.getModel(uri)
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshotEntry> = {}
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea._URI.fsPath !== uri.fsPath) continue
snapshottedDiffAreaOfId[diffareaid] = deepClone(
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
) as DiffAreaSnapshotEntry
}
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
// this._noLongerNeedModelReference(uri)
return {
snapshottedDiffAreaOfId,
entireFileCode, // the whole file's code
}
}
private _restoreVoidFileSnapshot = async (uri: URI, snapshot: VoidFileSnapshot) => {
// for each diffarea in this uri, stop streaming if currently streaming
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea.type === 'DiffZone')
this._stopIfStreaming(diffArea)
}
// delete all diffareas on this uri (clearing their styles)
this._deleteAllDiffAreas(uri)
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
// restore diffAreaOfId and diffAreasOfModelId
for (const diffareaid in snapshottedDiffAreaOfId) {
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
if (snapshottedDiffArea.type === 'DiffZone') {
this.diffAreaOfId[diffareaid] = {
...snapshottedDiffArea as DiffAreaSnapshotEntry<DiffZone>,
type: 'DiffZone',
_diffOfId: {},
_URI: uri,
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
_removeStylesFns: new Set(),
}
}
else if (snapshottedDiffArea.type === 'CtrlKZone') {
this.diffAreaOfId[diffareaid] = {
...snapshottedDiffArea as DiffAreaSnapshotEntry<CtrlKZone>,
_URI: uri,
_removeStylesFns: new Set<Function>(),
_mountInfo: null,
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
}
}
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
}
this._onDidAddOrDeleteDiffZones.fire({ uri })
// restore file content
this._writeURIText(uri, entireModelCode,
'wholeFileRange',
{ shouldRealignDiffAreas: false }
)
// this._noLongerNeedModelReference(uri)
}
private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) {
const getCurrentSnapshot = (): HistorySnapshot => {
const { model } = this._voidModelService.getModel(uri)
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea._URI.fsPath !== uri.fsPath) continue
snapshottedDiffAreaOfId[diffareaid] = deepClone(
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
) as DiffAreaSnapshot
}
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
// this._noLongerNeedModelReference(uri)
return {
snapshottedDiffAreaOfId,
entireFileCode, // the whole file's code
}
}
const restoreDiffAreas = async (snapshot: HistorySnapshot) => {
// for each diffarea in this uri, stop streaming if currently streaming
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea.type === 'DiffZone')
this._stopIfStreaming(diffArea)
}
// delete all diffareas on this uri (clearing their styles)
this._deleteAllDiffAreas(uri)
this.diffAreasOfURI[uri.fsPath]?.clear()
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
// restore diffAreaOfId and diffAreasOfModelId
for (const diffareaid in snapshottedDiffAreaOfId) {
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
if (snapshottedDiffArea.type === 'DiffZone') {
this.diffAreaOfId[diffareaid] = {
...snapshottedDiffArea as DiffAreaSnapshot<DiffZone>,
type: 'DiffZone',
_diffOfId: {},
_URI: uri,
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
_removeStylesFns: new Set(),
}
}
else if (snapshottedDiffArea.type === 'CtrlKZone') {
this.diffAreaOfId[diffareaid] = {
...snapshottedDiffArea as DiffAreaSnapshot<CtrlKZone>,
_URI: uri,
_removeStylesFns: new Set<Function>(),
_mountInfo: null,
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
}
}
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
}
this._onDidAddOrDeleteDiffZones.fire({ uri })
// restore file content
this._writeURIText(uri, entireModelCode,
'wholeFileRange',
{ shouldRealignDiffAreas: false }
)
// this._noLongerNeedModelReference(uri)
}
const beforeSnapshot: HistorySnapshot = getCurrentSnapshot()
let afterSnapshot: HistorySnapshot | null = null
const beforeSnapshot: VoidFileSnapshot = this._getCurrentVoidFileSnapshot(uri)
let afterSnapshot: VoidFileSnapshot | null = null
const elt: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: uri,
label: 'Void Agent',
code: 'undoredo.editCode',
undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); },
redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) }
undo: async () => { opts?.onWillUndo?.(); await this._restoreVoidFileSnapshot(uri, beforeSnapshot) },
redo: async () => { if (afterSnapshot) await this._restoreVoidFileSnapshot(uri, afterSnapshot) }
}
this._undoRedoService.pushElement(elt)
const onFinishEdit = async () => {
afterSnapshot = getCurrentSnapshot()
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
})
afterSnapshot = this._getCurrentVoidFileSnapshot(uri)
await this._voidModelService.saveModel(uri)
}
return { onFinishEdit }
}
public getVoidFileSnapshot(uri: URI) {
return this._getCurrentVoidFileSnapshot(uri)
}
public restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void {
this._restoreVoidFileSnapshot(uri, snapshot)
}
// delete diffOfId and diffArea._diffOfId
private _deleteDiff(diff: Diff) {
const diffArea = this.diffAreaOfId[diff.diffareaid]
@@ -886,6 +835,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
else if (diffArea.type === 'CtrlKZone')
this._deleteCtrlKZone(diffArea)
})
this.diffAreasOfURI[uri.fsPath]?.clear()
}
private _addOrInitializeDiffAreaAtURI = (uri: URI, diffareaid: string | number) => {
@@ -994,7 +944,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (diffArea?.type !== 'DiffZone') continue
// fire changed diffs (this is the only place Diffs are added)
if (!diffArea._streamState.isStreaming) {
this._onDidChangeDiffsInDiffZone.fire({ uri, diffareaid: diffArea.diffareaid })
this._onDidChangeDiffsInDiffZoneNotStreaming.fire({ uri, diffareaid: diffArea.diffareaid })
}
}
}
@@ -1160,29 +1110,51 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _getURIBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
// SR
if (opts.from === 'ClickApply') {
const uri = this._uriOfGivenURI(opts.uri)
if (!uri) return
return uri
}
else if (opts.from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone?.type !== 'CtrlKZone') return
const { _URI: uri } = ctrlKZone
return uri
}
return
}
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
await this._voidModelService.initializeModel(uri)
await this._voidModelService.saveModel(uri) // save the URI
}
// the applyDonePromise this returns can reject, and should be caught with .catch
public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null> {
public startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null {
let res: [DiffZone, Promise<void>] | undefined = undefined
if (opts.from === 'QuickEdit') {
res = await this._initializeWriteoverStream(opts) // rewrite
res = this._initializeWriteoverStream(opts) // rewrite
}
else if (opts.from === 'ClickApply') {
if (this._settingsService.state.globalSettings.enableFastApply) {
const numCharsInFile = this._fileLengthOfGivenURI(opts.uri)
if (numCharsInFile === null) return null
if (numCharsInFile < 1000) { // slow apply for short files (especially important for empty files)
res = await this._initializeWriteoverStream(opts)
res = this._initializeWriteoverStream(opts)
}
else {
res = await this._initializeSearchAndReplaceStream(opts) // fast apply
res = this._initializeSearchAndReplaceStream(opts) // fast apply
}
}
else {
res = await this._initializeWriteoverStream(opts) // rewrite
res = this._initializeWriteoverStream(opts) // rewrite
}
}
@@ -1308,19 +1280,21 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise<void>] | undefined> {
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
let uri: URI
let startRange: 'fullFile' | [number, number]
let ctrlKZoneIfQuickEdit: CtrlKZone | null = null
if (from === 'ClickApply') {
const uri_ = this._uriOfGivenURI(opts.uri)
if (!uri_) return
uri = uri_
startRange = 'fullFile'
}
else if (from === 'QuickEdit') {
@@ -1328,15 +1302,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone?.type !== 'CtrlKZone') return
ctrlKZoneIfQuickEdit = ctrlKZone
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
uri = _URI
const { startLine: startLine_, endLine: endLine_ } = ctrlKZone
startRange = [startLine_, endLine_]
}
else {
throw new Error(`Void: diff.type not recognized on: ${from}`)
}
await this._voidModelService.initializeModel(uri)
const { model } = this._voidModelService.getModel(uri)
if (!model) return
@@ -1348,12 +1320,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
@@ -1363,12 +1339,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`) }
@@ -1411,7 +1392,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// throws
const onError = (e: { message: string; fullError: Error | null; }) => {
this._notifyError(e)
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError
@@ -1432,10 +1413,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[modelSelection.providerName]?.[modelSelection.modelName] : undefined
// allowed to throw errors - this is called inside a promise that handles everything
const runWriteover = async () => {
let shouldSendAnotherMessage = true
@@ -1458,6 +1435,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
const { fullText: fullText_ } = params
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
@@ -1530,13 +1509,15 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise<void>] | undefined> {
const { from, applyStr, uri: givenURI, } = opts
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._uriOfGivenURI(givenURI)
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
await this._voidModelService.initializeModel(uri)
const { model } = this._voidModelService.getModel(uri)
if (!model) return
@@ -1546,10 +1527,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,20 +1577,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
const errContentOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => {
const errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => {
const descStr = str === `Not found` ?
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: str === `Not unique` ?
`The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: ``
: str === 'Has overlap' ?
`The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: ``
// string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has
// const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n')
// const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : ''
// const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : ''
// const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}`
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error.'
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
const errMsg = `${descStr}\n${soFarStr}`
return errMsg
@@ -1625,7 +1611,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
const onError = (e: { message: string; fullError: Error | null; }) => {
this._notifyError(e)
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message) // throw error h
@@ -1641,11 +1627,7 @@ 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[modelSelection.providerName]?.[modelSelection.modelName] : undefined
const N_RETRIES = 5
const N_RETRIES = 4
// allowed to throw errors - this is called inside a promise that handles everything
const runSearchReplace = async () => {
@@ -1676,6 +1658,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
const { fullText } = params
// blocks are [done done done ... {writingFinal|writingOriginal}]
@@ -1691,7 +1675,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// update stream state to the first line of original if some portion of original has been written
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine)
const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine)
if (typeof originalRange !== 'string') {
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
diffZone._streamState.line = startLine
@@ -1717,19 +1701,27 @@ class EditCodeService extends Disposable implements IEditCodeService {
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
const originalBounds = findTextInCode(block.orig, originalFileCode)
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
// if error
if (typeof originalBounds === 'string') {
// Check for overlap with existing modified ranges
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
const [existingStart, existingEnd] = trackingZone.metadata.originalBounds;
const hasNoOverlap = endLine < existingStart || startLine > existingEnd
return !hasNoOverlap
});
if (typeof originalBounds === 'string' || hasOverlap) {
const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const
console.log('--------------Error finding text in code:')
console.log('originalFileCode', { originalFileCode })
console.log('fullText', { fullText })
console.log('error:', originalBounds)
console.log('error:', errorMessage)
console.log('block.orig:', block.orig)
console.log('---------')
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
const content = errContentOfInvalidStr(errorMessage, block.orig)
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
)
@@ -1757,14 +1749,15 @@ class EditCodeService extends Disposable implements IEditCodeService {
return
}
console.log('---------adding-------')
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
console.log('block', deepClone(block))
console.log('origBounds', originalBounds)
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
console.log('start end', startLine, endLine)
// console.log('---------adding-------')
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
// console.log('block', deepClone(block))
// console.log('origBounds', originalBounds)
// console.log('start end', startLine, endLine)
// otherwise if no error, add the position as a diffarea
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
@@ -1821,7 +1814,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
onFinalMessage: async (params) => {
const { fullText } = params
console.log('DONE - editCode!', { fullText })
// 1. wait 500ms and fix lint errors - call lint error workflow
// (update react state to say "Fixing errors")
@@ -1836,10 +1828,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
// IMPORTANT - sort by lineNum
addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0])
const { model } = this._voidModelService.getModel(uri)
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
console.log('blocks', deepClone(blocks))
// const { model } = this._voidModelService.getModel(uri)
// console.log('DONE - editCode!', { fullText })
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
// console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
// console.log('blocks', deepClone(blocks))
for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata
@@ -1934,6 +1927,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
interruptURIStreaming({ uri }: { uri: URI }) {
if (!this._uriIsStreaming(uri)) return
this._undoHistory(uri)
// brute force for now is OK
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
@@ -1941,7 +1936,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (!diffArea._streamState.isStreaming) continue
this._stopIfStreaming(diffArea)
}
this._undoHistory(uri)
}
@@ -7,13 +7,20 @@ import { Event } from '../../../../base/common/event.js';
import { URI } from '../../../../base/common/uri.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Diff, DiffArea } from './editCodeService.js';
import { Diff, DiffArea, VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'
export type StartApplyingOpts = ({
export type CallBeforeStartApplyingOpts = {
from: 'QuickEdit';
diffareaid: number; // id of the CtrlK area (contains text selection)
} | {
from: 'ClickApply';
uri: 'current' | URI;
}
export type StartApplyingOpts = {
from: 'QuickEdit';
diffareaid: number; // id of the CtrlK area (contains text selection)
startBehavior: StartBehavior;
@@ -22,9 +29,7 @@ export type StartApplyingOpts = ({
applyStr: string;
uri: 'current' | URI;
startBehavior: StartBehavior;
})
}
export type AddCtrlKOpts = {
startLine: number,
@@ -37,7 +42,8 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
export interface IEditCodeService {
readonly _serviceBrand: undefined;
startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null>;
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
@@ -49,7 +55,7 @@ export interface IEditCodeService {
// events
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
onDidChangeDiffsInDiffZoneNotStreaming: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>;
onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>;
@@ -61,4 +67,6 @@ export interface IEditCodeService {
interruptURIStreaming(opts: { uri: URI }): void;
// testDiffs(): void;
getVoidFileSnapshot(uri: URI): VoidFileSnapshot;
restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void;
}
@@ -3,34 +3,9 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ComputedDiff } from '../../common/editCodeServiceTypes.js';
import { diffLines } from '../react/out/diff/index.js'
export type ComputedDiff = {
type: 'edit';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
code: string;
startLine: number; // 1-indexed
endLine: number;
} | {
type: 'insertion';
// originalCode: string;
originalStartLine: number; // insertion starts on column 0 of this
// originalEndLine: number;
code: string;
startLine: number;
endLine: number;
} | {
type: 'deletion';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
// code: string;
startLine: number; // deletion starts on column 0 of this
// endLine: number;
}
export function findDiffs(oldStr: string, newStr: string) {
// this makes it so the end of the file always ends with a \n (if you don't have this, then diffing E vs E\n gives an "edit". With it, you end up diffing E\n vs E\n\n which now properly gives an insertion)
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
.monaco-editor .void-sweepIdxBG {
.monaco-editor .void-sweepIdxBG {
background-color: var(--vscode-void-sweepIdxBG);
}
@@ -23,10 +23,30 @@
background-color: var(--vscode-void-redBG);
}
.void-watermark-button {
margin: 8px 0;
/* Renamed from void-watermark-button to void-openfolder-button */
.void-openfolder-button {
padding: 8px 20px;
background-color: #3b82f6;
background-color: #306dce;
color: white;
border: none;
border-radius: 4px;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
transition: background-color 0.2s ease;
}
.void-openfolder-button:hover {
background-color: #2563eb;
}
.void-openfolder-button:active {
background-color: #2563eb;
}
/* Added for Open SSH button with slightly darker color */
.void-openssh-button {
padding: 8px 20px;
background-color: #656565; /* Slightly darker than the #5a5a5a in the TS file */
color: white;
border: none;
border-radius: 4px;
@@ -35,134 +55,150 @@
cursor: pointer;
transition: background-color 0.2s ease;
}
.void-watermark-button:hover {
background-color: #2563eb;
.void-openssh-button:hover {
background-color: #474747; /* Darker on hover */
}
.void-watermark-button:active {
background-color: #2563eb;
.void-openssh-button:active {
background-color: #474747;
}
.void-settings-watermark-button {
margin: 8px 0;
padding: 8px 20px;
background-color: var(--vscode-input-background);
margin: 8px 0;
padding: 8px 20px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: none;
border-radius: 4px;
outline: none !important;
border: none;
border-radius: 4px;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
transition: all 0.2s ease;
cursor: pointer;
transition: all 0.2s ease;
}
.void-settings-watermark-button:hover {
filter: brightness(1.1);
}
.void-settings-watermark-button:active {
filter: brightness(1.1);
}
.void-link {
color: #3b82f6;
cursor: pointer;
transition: all 0.2s ease;
}
.void-link:hover {
opacity: 80%;
}
/* styles for all containers used by void */
.void-scope {
--scrollbar-vertical-width: 8px;
--scrollbar-horizontal-height: 6px;
}
/* Target both void-scope and all its descendants with scrollbars */
.void-scope,
.void-scope * {
scrollbar-width: thin !important;
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important;
/* For Firefox */
}
.void-scope::-webkit-scrollbar,
.void-scope *::-webkit-scrollbar {
width: var(--scrollbar-vertical-width) !important;
height: var(--scrollbar-horizontal-height) !important;
background-color: var(--void-bg-3) !important;
}
.void-scope::-webkit-scrollbar-thumb,
.void-scope *::-webkit-scrollbar-thumb {
background-color: var(--void-bg-1) !important;
border-radius: 4px !important;
border: none !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
.void-scope::-webkit-scrollbar-thumb:hover,
.void-scope *::-webkit-scrollbar-thumb:hover {
background-color: var(--void-bg-1) !important;
filter: brightness(1.1) !important;
}
.void-scope::-webkit-scrollbar-thumb:active,
.void-scope *::-webkit-scrollbar-thumb:active {
background-color: var(--void-bg-1) !important;
filter: brightness(1.2) !important;
}
.void-scope::-webkit-scrollbar-track,
.void-scope *::-webkit-scrollbar-track {
background-color: var(--void-bg-3) !important;
border: none !important;
}
.void-scope::-webkit-scrollbar-corner,
.void-scope *::-webkit-scrollbar-corner {
background-color: var(--void-bg-3) !important;
}
/* Add void-scrollable-element styles to match */
.void-scrollable-element {
background-color: var(--vscode-editor-background);
--scrollbar-vertical-width: 14px;
--scrollbar-horizontal-height: 6px;
overflow: auto;
/* Ensure scrollbars are shown when needed */
}
.void-scrollable-element,
.void-scrollable-element * {
scrollbar-width: thin !important;
/* For Firefox */
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important;
/* For Firefox */
}
.void-scrollable-element::-webkit-scrollbar,
.void-scrollable-element *::-webkit-scrollbar {
width: 14px !important;
height: 4px !important;
}
.void-scrollable-element::-webkit-scrollbar-track,
.void-scrollable-element *::-webkit-scrollbar-track {
background: transparent !important;
width: var(--scrollbar-vertical-width) !important;
height: var(--scrollbar-horizontal-height) !important;
background-color: var(--void-bg-3) !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb,
.void-scrollable-element *::-webkit-scrollbar-thumb {
background-color: transparent !important;
border-radius: 0px !important;
background-color: var(--void-bg-1) !important;
border-radius: 4px !important;
border: none !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
background-color: var(--void-bg-1) !important;
filter: brightness(1.1) !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb:active,
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
background-color: var(--void-bg-1) !important;
filter: brightness(1.2) !important;
}
.void-scrollable-element::-webkit-scrollbar-track,
.void-scrollable-element *::-webkit-scrollbar-track {
background-color: var(--void-bg-3) !important;
border: none !important;
}
.void-scrollable-element::-webkit-scrollbar-corner,
.void-scrollable-element *::-webkit-scrollbar-corner {
background-color: transparent !important;
}
.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
background-color: var(--vscode-scrollbarSlider-background) !important;
background-color: var(--void-bg-3) !important;
}
@@ -74,7 +74,7 @@ function saveStylesFile() {
} catch (err) {
console.error('[scope-tailwind] Error saving styles.css:', err);
}
}, 4000);
}, 6000);
}
const args = process.argv.slice(2);
@@ -1,12 +1,17 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
import { usePromise, useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
import { PlacesType, VariantType } from 'react-tooltip'
enum CopyButtonText {
Idle = 'Copy',
@@ -16,30 +21,28 @@ enum CopyButtonText {
type IconButtonProps = {
onClick: () => void;
Icon: LucideIcon
disabled?: boolean
className?: string
}
export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => (
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
disabled={disabled}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onClick?.();
onClick?.(e);
}}
// border border-void-border-1 rounded
className={`
size-[22px]
p-[4px]
size-[18px]
p-[2px]
flex items-center justify-center
text-sm bg-void-bg-3 text-void-fg-1
text-sm text-void-fg-3
hover:brightness-110
border border-void-border-1 rounded
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
{...props}
>
<Icon />
</button>
@@ -64,9 +67,9 @@ export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonPro
// </button>
// )
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const COPY_FEEDBACK_TIMEOUT = 1500 // amount of time to say 'Copied!'
const CopyButton = ({ codeStr }: { codeStr: string }) => {
export const CopyButton = ({ codeStr, toolTipName }: { codeStr: string | (() => Promise<string> | string), toolTipName: string }) => {
const accessor = useAccessor()
const metricsService = accessor.get('IMetricsService')
@@ -80,8 +83,8 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
}, COPY_FEEDBACK_TIMEOUT)
}, [copyButtonText])
const onCopy = useCallback(() => {
clipboardService.writeText(codeStr)
const onCopy = useCallback(async () => {
clipboardService.writeText(typeof codeStr === 'string' ? codeStr : await codeStr())
.then(() => { setCopyButtonText(CopyButtonText.Copied) })
.catch(() => { setCopyButtonText(CopyButtonText.Error) })
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
@@ -90,18 +93,14 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
return <IconShell1
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
onClick={onCopy}
{...tooltipPropsForApplyBlock({ tooltipName: toolTipName })}
/>
}
// state persisted for duration of react only
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
@@ -111,185 +110,229 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
onClick={() => {
commandService.executeCommand('vscode.open', uri, { preview: true })
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
{...props}
/>
)
return jumpToFileButton
}
export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
return (
<IconShell1
Icon={Terminal}
onClick={onClick}
/>
)
}
// state persisted for duration of react only
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
const getUriBeingApplied = (applyBoxId: string) => {
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const metricsService = accessor.get('IMetricsService')
const [_, rerender] = useState(0)
const getUriBeingApplied = useCallback(() => {
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}, [applyBoxId])
const getStreamState = useCallback(() => {
const uri = getUriBeingApplied()
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return 'idle-no-changes'
return voidCommandBarService.getStreamState(uri)
}, [voidCommandBarService, getUriBeingApplied])
}, [voidCommandBarService, applyBoxId])
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = (
getUriBeingApplied()?.fsPath === uri_.fsPath
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
)
if (!shouldUpdate) return
rerender(c => c + 1)
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
if (shouldUpdate) {
rerender(c => c + 1)
}
}, [applyBoxId, uri]))
const currStreamState = getStreamState()
return {
getStreamState,
isDisabled,
currStreamState,
}
}
type IndicatorColor = 'green' | 'orange' | 'dark' | 'yellow' | null
export const StatusIndicator = ({ indicatorColor, title, className, ...props }: { indicatorColor: IndicatorColor, title?: React.ReactNode, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
return (
<div className={`flex flex-row text-void-fg-3 text-xs items-center gap-1.5 ${className}`} {...props}>
{title && <span className='opacity-80'>{title}</span>}
<div
className={` size-1.5 rounded-full border
${indicatorColor === 'dark' ? 'bg-void-bg-3 border-void-border-1' :
indicatorColor === 'orange' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
indicatorColor === 'green' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
indicatorColor === 'yellow' ? 'bg-yellow-500 border-yellow-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
'bg-void-border-1 border-void-border-1'
}
`}
/>
</div>
);
};
const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position = 'top', offset = undefined }: { tooltipName: string, color?: IndicatorColor, position?: PlacesType, offset?: number }) => ({
'data-tooltip-id': color === 'orange' ? `void-tooltip-orange` : color === 'green' ? 'void-tooltip-green' : 'void-tooltip',
'data-tooltip-place': position as PlacesType,
'data-tooltip-content': `${tooltipName}`,
'data-tooltip-offset': offset,
})
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const color = (
currStreamState === 'idle-no-changes' ? 'dark' :
currStreamState === 'streaming' ? 'orange' :
currStreamState === 'idle-has-changes' ? 'green' :
null
)
const tooltipName = (
currStreamState === 'idle-no-changes' ? 'Done' :
currStreamState === 'streaming' ? 'Applying' :
currStreamState === 'idle-has-changes' ? 'Done' : // also 'Done'? 'Applied' looked bad
''
)
const statusIndicatorHTML = <StatusIndicator
key={currStreamState}
className='mx-2'
indicatorColor={color}
{...tooltipPropsForApplyBlock({ tooltipName, color, position: 'top', offset: 12 })}
/>
return statusIndicatorHTML
}
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const {
currStreamState,
isDisabled,
getStreamState,
} = useApplyButtonState({ applyBoxId, uri })
const onClickSubmit = useCallback(async () => {
if (isDisabled) return
if (getStreamState() === 'streaming') return
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
const opts = {
from: 'ClickApply',
applyStr: codeStr,
uri: uri,
startBehavior: 'keep-conflicts',
}) ?? []
startBehavior: 'reject-conflicts',
} as const
await editCodeService.callBeforeStartApplying(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
applyDonePromise?.catch(e => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
rerender(c => c + 1)
// rerender(c => c + 1)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
const onInterrupt = useCallback(() => {
if (getStreamState() !== 'streaming') return
const uri = getUriBeingApplied()
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return
editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {})
}, [getStreamState, getUriBeingApplied, editCodeService, metricsService])
}, [getStreamState, applyBoxId, editCodeService, metricsService])
const onAccept = useCallback(() => {
const uri = getUriBeingApplied()
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [getUriBeingApplied, editCodeService])
}, [applyBoxId, editCodeService])
const onReject = useCallback(() => {
const uri = getUriBeingApplied()
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [getUriBeingApplied, editCodeService])
}, [applyBoxId, editCodeService])
const onReapply = useCallback(() => {
onReject()
onClickSubmit()
}, [onReject, onClickSubmit])
// const onReapply = useCallback(() => {
// onReject()
// onClickSubmit()
// }, [onReject, onClickSubmit])
const currStreamState = getStreamState()
const copyButton = (
<CopyButton codeStr={codeStr} />
)
const playButton = (
<IconShell1
Icon={Play}
onClick={onClickSubmit}
/>
)
const stopButton = (
<IconShell1
Icon={Square}
onClick={onInterrupt}
/>
)
const reapplyButton = (
<IconShell1
Icon={RotateCw}
onClick={onReapply}
/>
)
const acceptButton = (
<IconShell1
Icon={Check}
onClick={onAccept}
className="text-green-600"
/>
)
const rejectButton = (
<IconShell1
Icon={X}
onClick={onReject}
className="text-red-600"
/>
)
let buttonsHTML = <></>
if (currStreamState === 'streaming') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{copyButton}
{stopButton}
</>
return <IconShell1
Icon={Square}
onClick={onInterrupt}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
}
if (currStreamState === 'idle-no-changes') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{copyButton}
{playButton}
</>
return <IconShell1
Icon={reapplyIcon ? RotateCw : Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: reapplyIcon ? 'Reapply' : 'Apply' })}
/>
}
if (currStreamState === 'idle-has-changes') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{reapplyButton}
{rejectButton}
{acceptButton}
return <>
{/* <IconShell1
Icon={RotateCw}
onClick={onReapply}
/> */}
<IconShell1
Icon={X}
onClick={onReject}
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
/>
<IconShell1
Icon={Check}
onClick={onAccept}
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
/>
</>
}
const statusIndicatorHTML = <div className='flex flex-row items-center min-h-4 max-h-4 min-w-4 max-w-4'>
<div
className={` size-1.5 rounded-full border
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
'bg-void-border-1 border-void-border-1'
}`
}
/>
</div>
return {
statusIndicatorHTML,
buttonsHTML,
}
}
export const BlockCodeApplyWrapper = ({
children,
initValue,
@@ -305,10 +348,10 @@ export const BlockCodeApplyWrapper = ({
language: string;
uri: URI | 'current',
}) => {
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const name = uri !== 'current' ?
<ListableToolItem
@@ -324,13 +367,15 @@ export const BlockCodeApplyWrapper = ({
{/* header */}
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
<div className="flex items-center">
{statusIndicatorHTML}
<StatusIndicatorForApplyButton uri={uri} applyBoxId={applyBoxId} />
<span className="text-[13px] font-light text-void-fg-3">
{name}
</span>
</div>
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
{buttonsHTML}
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} reapplyIcon={false} />
</div>
</div>
@@ -31,6 +31,63 @@ function isValidUri(s: string): boolean {
return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like //
}
// renders contiguous string of latex eg $e^{i\pi}$
const LatexRender = ({ latex }: { latex: string }) => {
return <span className="katex-error text-red-500">{latex}</span>
// try {
// let formula = latex;
// let displayMode = false;
// // Extract the formula from delimiters
// if (latex.startsWith('$') && latex.endsWith('$')) {
// // Check if it's display math $$...$$
// if (latex.startsWith('$$') && latex.endsWith('$$')) {
// formula = latex.slice(2, -2);
// displayMode = true;
// } else {
// formula = latex.slice(1, -1);
// }
// } else if (latex.startsWith('\\(') && latex.endsWith('\\)')) {
// formula = latex.slice(2, -2);
// } else if (latex.startsWith('\\[') && latex.endsWith('\\]')) {
// formula = latex.slice(2, -2);
// displayMode = true;
// }
// // Render LaTeX
// const html = katex.renderToString(formula, {
// displayMode: displayMode,
// throwOnError: false,
// output: 'html'
// });
// // Sanitize the HTML output with DOMPurify
// const sanitizedHtml = dompurify.sanitize(html, {
// RETURN_TRUSTED_TYPE: true,
// USE_PROFILES: { html: true, svg: true, mathMl: true }
// });
// // Add proper styling based on mode
// const className = displayMode
// ? 'katex-block my-2 text-center'
// : 'katex-inline';
// // Use the ref approach to avoid dangerouslySetInnerHTML
// const mathRef = React.useRef<HTMLSpanElement>(null);
// React.useEffect(() => {
// if (mathRef.current) {
// mathRef.current.innerHTML = sanitizedHtml as unknown as string;
// }
// }, [sanitizedHtml]);
// return <span ref={mathRef} className={className}></span>;
// } catch (error) {
// console.error('KaTeX rendering error:', error);
// return <span className="katex-error text-red-500">{latex}</span>;
// }
}
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
@@ -57,7 +114,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 })
@@ -108,6 +165,105 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
}
const paragraphToLatexSegments = (paragraphText: string) => {
const segments: React.ReactNode[] = [];
if (paragraphText
&& !(paragraphText.includes('#') || paragraphText.includes('`')) // don't process latex if a codespan or header tag
&& !/^[\w\s.()[\]{}]+$/.test(paragraphText) // don't process latex if string only contains alphanumeric chars, whitespace, periods, and brackets
) {
const rawText = paragraphText;
// Regular expressions to match LaTeX delimiters
const displayMathRegex = /\$\$(.*?)\$\$/g; // Display math: $$...$$
const inlineMathRegex = /\$((?!\$).*?)\$/g; // Inline math: $...$ (but not $$)
// Check if the paragraph contains any LaTeX expressions
if (displayMathRegex.test(rawText) || inlineMathRegex.test(rawText)) {
// Reset the regex state (since we used .test earlier)
displayMathRegex.lastIndex = 0;
inlineMathRegex.lastIndex = 0;
// Parse the text into segments of regular text and LaTeX
let lastIndex = 0;
let segmentId = 0;
// First replace display math ($$...$$)
let match;
while ((match = displayMathRegex.exec(rawText)) !== null) {
const [fullMatch, formula] = match;
const matchIndex = match.index;
// Add text before the LaTeX expression
if (matchIndex > lastIndex) {
const textBefore = rawText.substring(lastIndex, matchIndex);
segments.push(
<span key={`text-${segmentId++}`}>
{textBefore}
</span>
);
}
// Add the LaTeX expression
segments.push(
<LatexRender key={`latex-${segmentId++}`} latex={fullMatch} />
);
lastIndex = matchIndex + fullMatch.length;
}
// Add any remaining text (which might contain inline math)
if (lastIndex < rawText.length) {
const remainingText = rawText.substring(lastIndex);
// Process inline math in the remaining text
lastIndex = 0;
inlineMathRegex.lastIndex = 0;
const inlineSegments: React.ReactNode[] = [];
while ((match = inlineMathRegex.exec(remainingText)) !== null) {
const [fullMatch] = match;
const matchIndex = match.index;
// Add text before the inline LaTeX
if (matchIndex > lastIndex) {
const textBefore = remainingText.substring(lastIndex, matchIndex);
inlineSegments.push(
<span key={`inline-text-${segmentId++}`}>
{textBefore}
</span>
);
}
// Add the inline LaTeX
inlineSegments.push(
<LatexRender key={`inline-latex-${segmentId++}`} latex={fullMatch} />
);
lastIndex = matchIndex + fullMatch.length;
}
// Add any remaining text after all inline math
if (lastIndex < remainingText.length) {
inlineSegments.push(
<span key={`inline-final-${segmentId++}`}>
{remainingText.substring(lastIndex)}
</span>
);
}
segments.push(...inlineSegments);
}
}
}
return segments
}
export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean }
const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): React.ReactNode => {
const accessor = useAccessor()
@@ -120,11 +276,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 +308,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 +335,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,25 +344,26 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
</HeadingTag>
}
if (t.type === "table") {
if (t.type === 'table') {
return (
<div>
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index}>
{cell.raw}
{t.header.map((h, hIdx: number) => (
<th key={hIdx}>
{h.text}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td key={cellIndex} >
{cell.raw}
{t.rows.map((row, rowIdx: number) => (
<tr key={rowIdx}>
{row.map((r, rIdx: number) => (
<td key={rIdx} >
{r.text}
</td>
))}
</tr>
@@ -217,14 +374,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 +394,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 +408,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 +444,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
)
}
if (t.type === "paragraph") {
if (t.type === 'paragraph') {
// check for latex
const latexSegments = paragraphToLatexSegments(t.raw)
if (latexSegments.length !== 0) {
if (inPTag) {
return <span className='block'>{latexSegments}</span>;
}
return <p>{latexSegments}</p>;
}
// if no latex, default behavior
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index}
@@ -304,34 +472,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 +502,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 +525,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>
)
}
@@ -390,4 +552,3 @@ export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation
</>
)
}
@@ -10,7 +10,6 @@ import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
import { useRefState } from '../util/helpers.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
export const QuickEditChat = ({
@@ -63,11 +62,14 @@ export const QuickEditChat = ({
if (isStreamingRef.current) return
textAreaFnsRef.current?.disable()
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
const opts = {
from: 'QuickEdit',
diffareaid,
startBehavior: 'keep-conflicts',
}) ?? []
} as const
await editCodeService.callBeforeStartApplying(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })
@@ -86,8 +88,6 @@ export const QuickEditChat = ({
editCodeService.removeCtrlKZone({ diffareaid })
}, [editCodeService, diffareaid])
useScrollbarStyles(sizerRef)
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
const chatAreaRef = useRef<HTMLDivElement | null>(null)
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorDisplay } from './ErrorDisplay.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
interface Props {
children: ReactNode;
@@ -51,11 +51,12 @@ class ErrorBoundary extends Component<Props, State> {
// Use ErrorDisplay component as the default error UI
return (
<ErrorDisplay
message={this.state.error + ''}
fullError={this.state.error}
onDismiss={this.props.onDismiss || null}
/>
<WarningBox text={this.state.error + ''} />
// <ErrorDisplay
// message={this.state.error + ''}
// fullError={this.state.error}
// onDismiss={this.props.onDismiss || null}
// />
);
}
File diff suppressed because it is too large Load Diff
@@ -3,35 +3,20 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React from "react";
import { useAccessor, useChatThreadsState } from '../util/services.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { useMemo, useState } from 'react';
import { CopyButton, IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useFullChatThreadsStreamState, useSettingsState } from '../util/services.js';
import { IconX } from './SidebarChat.js';
import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserCheck, X } from 'lucide-react';
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
export const OldSidebarThreadSelector = () => {
export const SidebarThreadSelector = () => {
const threadsState = useChatThreadsState()
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
return (
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
@@ -52,72 +37,369 @@ export const SidebarThreadSelector = () => {
</div>
{/* a list of all the past threads */}
<div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
{sortedThreadIds.length === 0
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const pastThread = allThreads[threadId];
if (!pastThread) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
let firstMsg = null;
// let secondMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role !== 'tool' && msg.role !== 'tool_request');
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
title={new Date(pastThread.lastModified).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
</div>
{/* <OldPastThreadsList /> */}
</div>
)
}
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
const OldPastThreadsList = () => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
return <div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
{sortedThreadIds.length === 0
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const pastThread = allThreads[threadId];
if (!pastThread) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
let firstMsg = null;
// let secondMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false })
}}
title={new Date(pastThread.lastModified).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
</div>
}
const numInitialThreads = 3
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
const [showAll, setShowAll] = useState(false);
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null)
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
const streamState = useFullChatThreadsStreamState()
const runningThreadIds: { [threadId: string]: IsRunningType | undefined } = {}
for (const threadId in streamState) {
const isRunning = streamState[threadId]?.isRunning
if (isRunning) { runningThreadIds[threadId] = isRunning }
}
if (!allThreads) {
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
}
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
// Get only first 5 threads if not showing all
const hasMoreThreads = sortedThreadIds.length > numInitialThreads;
const displayThreads = showAll ? sortedThreadIds : sortedThreadIds.slice(0, numInitialThreads);
return (
<div className={`flex flex-col mb-2 gap-2 w-full text-nowrap text-void-fg-3 select-none relative ${className}`}>
{displayThreads.length === 0
? <></> // No chats yet... Suggestion: Tell me about my codebase Suggestion: Create a new .voidrules file in the root of my repo
: displayThreads.map((threadId, i) => {
const pastThread = allThreads[threadId];
if (!pastThread) {
return <div key={i} className="p-1">{`Error accessing chat history.`}</div>;
}
return (
<PastThreadElement
key={pastThread.id}
pastThread={pastThread}
idx={i}
hoveredIdx={hoveredIdx}
setHoveredIdx={setHoveredIdx}
isRunning={runningThreadIds[pastThread.id]}
/>
);
})
}
{hasMoreThreads && !showAll && (
<div
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
onClick={() => setShowAll(true)}
>
Show {sortedThreadIds.length - numInitialThreads} more...
</div>
)}
{hasMoreThreads && showAll && (
<div
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
onClick={() => setShowAll(false)}
>
Show less
</div>
)}
</div>
);
};
// Format date to display as today, yesterday, or date
const formatDate = (date: Date) => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (date >= today) {
return 'Today';
} else if (date >= yesterday) {
return 'Yesterday';
} else {
return `${date.toLocaleString('default', { month: 'short' })} ${date.getDate()}`;
}
};
// Format time to 12-hour format
const formatTime = (date: Date) => {
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
};
const DuplicateButton = ({ threadId }: { threadId: string }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
return <IconShell1
Icon={Copy}
className='size-[11px]'
onClick={() => { chatThreadsService.duplicateThread(threadId); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Duplicate thread'
>
</IconShell1>
}
const TrashButton = ({ threadId }: { threadId: string }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const [isTrashPressed, setIsTrashPressed] = useState(false)
return (isTrashPressed ?
<div className='flex flex-nowrap text-nowrap gap-1'>
<IconShell1
Icon={X}
className='size-[11px]'
onClick={() => { setIsTrashPressed(false); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Cancel'
/>
<IconShell1
Icon={Check}
className='size-[11px]'
onClick={() => { chatThreadsService.deleteThread(threadId); setIsTrashPressed(false); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Confirm'
/>
</div>
: <IconShell1
Icon={Trash2}
className='size-[11px]'
onClick={() => { setIsTrashPressed(true); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Delete thread'
/>
)
}
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunning }: {
pastThread: ThreadType,
idx: number,
hoveredIdx: number | null,
setHoveredIdx: (idx: number | null) => void,
isRunning: IsRunningType | undefined,
}
) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
// const settingsState = useSettingsState()
// const convertService = accessor.get('IConvertToLLMMessageService')
// const chatMode = settingsState.globalSettings.chatMode
// const modelSelection = settingsState.modelSelectionOfFeature?.Chat ?? null
// const copyChatButton = <CopyButton
// codeStr={async () => {
// const { messages } = await convertService.prepareLLMChatMessages({
// chatMessages: currentThread.messages,
// chatMode,
// modelSelection,
// })
// return JSON.stringify(messages, null, 2)
// }}
// toolTipName={modelSelection === null ? 'Copy As Messages Payload' : `Copy As ${displayInfoOfProviderName(modelSelection.providerName).title} Payload`}
// />
// const currentThread = chatThreadsService.getCurrentThread()
// const copyChatButton2 = <CopyButton
// codeStr={async () => {
// return JSON.stringify(currentThread.messages, null, 2)
// }}
// toolTipName={`Copy As Void Chat`}
// />
let firstMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
if (firstUserMsgIdx !== -1) {
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx];
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
const detailsHTML = <span
className='gap-1 inline-flex items-center'
// data-tooltip-id='void-tooltip'
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
// data-tooltip-place='top'
>
{/* <span>{numMessages}</span> */}
{formatDate(new Date(pastThread.lastModified))}
</span>
return <div
key={pastThread.id}
className={`
py-1 px-2 rounded text-sm bg-zinc-700/5 hover:bg-zinc-700/10 dark:bg-zinc-300/5 dark:hover:bg-zinc-300/10 cursor-pointer opacity-80 hover:opacity-100
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false });
}}
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
>
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
{/* spinner */}
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
null}
{/* name */}
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
</span>
<div className="flex items-center gap-x-1 opacity-60">
{idx === hoveredIdx ?
<>
{/* trash icon */}
<DuplicateButton threadId={pastThread.id} />
{/* trash icon */}
<TrashButton threadId={pastThread.id} />
</>
: <>
{detailsHTML}
</>
}
</div>
</div>
</div>
}
@@ -11,7 +11,8 @@
--void-bg-1: var(--vscode-input-background);
--void-bg-1-alt: var(--vscode-badge-background);
--void-bg-2: var(--vscode-sideBar-background);
--void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
--void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%);
--void-bg-2-hover: color-mix(in srgb, var(--vscode-editor-foreground) 2%, var(--vscode-sideBar-background) 98%);
--void-bg-3: var(--vscode-editor-background);
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
@@ -48,6 +48,80 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
}
type GenerateNextOptions = (newPathText: string) => Option[]
type Option = {
name: string,
displayName: string,
} & (
| { nextOptions: Option[], generateNextOptions?: undefined }
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions }
| { nextOptions?: undefined, generateNextOptions?: undefined }
)
const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string[], newPathText: string) => {
const allOptions: Option[] = [
{
name: 'files',
displayName: 'files',
generateNextOptions: () => [
{ name: 'a.txt', displayName: 'a.txt', },
{ name: 'b.txt', displayName: 'b.txt', },
{ name: 'c.txt', displayName: 'c.txt', },
{ name: 'd.txt', displayName: 'd.txt', },
{ name: 'e.txt', displayName: 'e.txt', },
{ name: 'f.txt', displayName: 'f.txt', },
{ name: 'g.txt', displayName: 'g.txt', },
{ name: '!a.txt', displayName: '!a.txt', },
{ name: '!b.txt', displayName: '!b.txt', },
{ name: '!c.txt', displayName: '!c.txt', },
{ name: '!d.txt', displayName: '!d.txt', },
{ name: '!e.txt', displayName: '!e.txt', },
{ name: '!f.txt', displayName: '!f.txt', },
{ name: '!g.txt', displayName: '!g.txt', },
]
},
{
name: 'folders',
displayName: 'folders',
nextOptions: [
{ name: 'FOLDER', displayName: 'FOLDER', },
]
},
]
// follow the path in the optionsTree (until the last path element)
let nextOptionsAtPath = allOptions
let generateNextOptionsAtPath: GenerateNextOptions | undefined = undefined
for (const pn of path) {
const selectedOption = nextOptionsAtPath.find(o => o.name.toLowerCase() === pn.toLowerCase())
if (!selectedOption) return;
nextOptionsAtPath = selectedOption.nextOptions! // assume nextOptions exists until we hit the very last option (the path will never contain the last possible option)
generateNextOptionsAtPath = selectedOption.generateNextOptions
}
if (generateNextOptionsAtPath) {
nextOptionsAtPath = generateNextOptionsAtPath(newPathText)
}
const optionsAtPath = nextOptionsAtPath.filter(o => o.name.includes(newPathText))
return optionsAtPath
}
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
type InputBox2Props = {
@@ -64,8 +138,235 @@ type InputBox2Props = {
}
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
// mirrors whatever is in ref
const accessor = useAccessor()
const toolsService = accessor.get('IToolsService')
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const selectedOptionRef = useRef<HTMLDivElement>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [path, setPath] = useState<string[]>([]);
const [optionIdx, setOptionIdx] = useState<number>(0);
const [options, setOptions] = useState<Option[]>([]);
const [newPathText, setNewPathText] = useState<string>('');
const insertTextAtCursor = (text: string) => {
const textarea = textAreaRef.current;
if (!textarea) return;
// Focus the textarea first
textarea.focus();
// The most reliable way to simulate typing is to use execCommand
// which will trigger all the appropriate native events
document.execCommand('insertText', false, text);
// React's onChange relies on a SyntheticEvent system
// The best way to ensure it runs is to call callbacks directly
if (onChangeText) {
onChangeText(textarea.value);
}
adjustHeight();
};
const onSelectOption = () => {
if (!options.length) { return; }
const option = options[optionIdx];
const newPath = [...path, option.name]
const isLastOption = !option.generateNextOptions && !option.nextOptions
setPath(newPath)
setNewPathText('')
setOptionIdx(0)
if (isLastOption) {
setIsMenuOpen(false)
insertTextAtCursor(`TODO-${option.displayName}`)
}
else {
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
}
}
const onRemoveOption = () => {
const newPath = [...path.slice(0, path.length - 1)]
setPath(newPath)
setNewPathText('')
setOptionIdx(0)
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
}
const onOpenOptionMenu = () => {
setPath([])
setNewPathText('')
setIsMenuOpen(true);
setOptionIdx(0);
setOptions(getOptionsAtPath(accessor, [], '') || []);
}
const onCloseOptionMenu = () => {
setIsMenuOpen(false);
}
const onNavigateUp = () => {
if (options.length === 0) return;
setOptionIdx((prevIdx) => (prevIdx - 1 + options.length) % options.length);
}
const onNavigateDown = () => {
if (options.length === 0) return;
setOptionIdx((prevIdx) => (prevIdx + 1) % options.length);
}
const onPathTextChange = (newStr: string) => {
setNewPathText(newStr);
setOptions(getOptionsAtPath(accessor, path, newStr) || []);
}
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'ArrowUp') {
onNavigateUp();
} else if (e.key === 'ArrowDown') {
onNavigateDown();
} else if (e.key === 'ArrowLeft') {
onSelectOption();
} else if (e.key === 'ArrowRight') {
onSelectOption();
} else if (e.key === 'Enter') {
onSelectOption();
} else if (e.key === 'Escape') {
onCloseOptionMenu()
} else if (e.key === 'Backspace') {
if (!newPathText) { // No text remaining
if (path.length === 0) {
onCloseOptionMenu()
} else {
onRemoveOption();
}
}
else if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+Backspace
onPathTextChange('')
}
else { // Backspace
onPathTextChange(newPathText.slice(0, -1))
}
} else if (e.key.length === 1) {
if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+letter
// do nothing
}
else { // letter
onPathTextChange(newPathText + e.key)
}
}
e.preventDefault();
e.stopPropagation();
};
// scroll the selected optionIdx into view on optionIdx and newPathText changes
useEffect(() => {
if (isMenuOpen && selectedOptionRef.current) {
selectedOptionRef.current.scrollIntoView({
behavior: 'instant',
block: 'nearest',
inline: 'nearest',
});
}
}, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]);
const measureRef = useRef<HTMLDivElement>(null);
const gapPx = 2
const offsetPx = 2
const {
x,
y,
strategy,
refs,
middlewareData,
update
} = useFloating({
open: isMenuOpen,
onOpenChange: setIsMenuOpen,
placement: 'top',
middleware: [
offset({ mainAxis: gapPx, crossAxis: offsetPx }),
flip({
boundary: document.body,
padding: 8
}),
shift({
boundary: document.body,
padding: 8,
}),
size({
apply({ availableHeight, elements, rects }) {
const maxHeight = Math.min(availableHeight)
Object.assign(elements.floating.style, {
maxHeight: `${maxHeight}px`,
overflowY: 'auto',
// Ensure the width isn't constrained by the parent
width: `${Math.max(
rects.reference.width,
measureRef.current?.offsetWidth ?? 0
)}px`
});
},
padding: 8,
// Use viewport as boundary instead of any parent element
boundary: document.body,
}),
],
whileElementsMounted: autoUpdate,
strategy: 'fixed',
});
useEffect(() => {
if (!isMenuOpen) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
const floating = refs.floating.current;
const reference = refs.reference.current;
// Check if reference is an HTML element before using contains
const isReferenceHTMLElement = reference && 'contains' in reference;
if (
floating &&
(!isReferenceHTMLElement || !reference.contains(target)) &&
!floating.contains(target)
) {
setIsMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isMenuOpen, refs.floating, refs.reference]);
const [isEnabled, setEnabled] = useState(true)
const adjustHeight = useCallback(() => {
@@ -104,17 +405,20 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
return (
return <>
<textarea
autoFocus={false}
ref={useCallback((r: HTMLTextAreaElement | null) => {
if (fnsRef)
fnsRef.current = fns
refs.setReference(r)
textAreaRef.current = r
if (typeof ref === 'function') ref(r)
else if (ref) ref.current = r
adjustHeight()
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
}, [fnsRef, fns, setEnabled, adjustHeight, ref, refs])}
onFocus={onFocus}
onBlur={onBlur}
@@ -129,7 +433,16 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// inputBorder: asCssVariable(inputBorder),
}}
onChange={useCallback(() => {
onInput={useCallback((event: React.FormEvent<HTMLTextAreaElement>) => {
const latestChange = (event.nativeEvent as InputEvent).data;
if (latestChange === '@') {
onOpenOptionMenu()
}
}, [onOpenOptionMenu, accessor])}
onChange={useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const r = textAreaRef.current
if (!r) return
onChangeText?.(r.value)
@@ -137,28 +450,86 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
}, [onChangeText, adjustHeight])}
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (isMenuOpen) {
onMenuKeyDown(e)
return;
}
if (e.key === 'Enter') {
// Shift + Enter when multiline = newline
const shouldAddNewline = e.shiftKey && multiline
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
}
onKeyDown?.(e)
}, [onKeyDown, multiline])}
}, [onKeyDown, onMenuKeyDown, multiline])}
rows={1}
placeholder={placeholder}
/>
)
{/* <div>{`idx ${optionIdx}`}</div> */}
{isMenuOpen && (
<div
ref={refs.setFloating}
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0
}}
onWheel={(e) => e.stopPropagation()}
>
<div className="py-1">
{/* Path navigation breadcrumbs */}
<div className="px-2 py-1 text-void-fg-3 text-sm border-b border-void-border-3">
{[...path, newPathText].join(' > ')}
</div>
{/* Options list */}
{options.length === 0 ? (
<div className="px-3 py-2 text-void-fg-3">No options available</div>
) : (
options.map((o, oIdx) => (
<div
ref={oIdx === optionIdx ? selectedOptionRef : null}
key={o.name}
className={`px-3 py-1.5 cursor-pointer bg-void-bg-2 ${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}`}
onClick={() => { onSelectOption(); }}
>
<div className="flex items-center">
<span className="text-void-fg-1">{o.displayName}</span>
{o.nextOptions || o.generateNextOptions ? (
<svg className="ml-2 h-3 w-3 text-void-fg-3" viewBox="0 0 12 12" fill="none">
<path
d="M4.5 2.5L8 6L4.5 9.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : null}
</div>
</div>
))
)}
</div>
</div>
)}
</>
})
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, ...inputProps }: {
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, compact, ...inputProps }: {
value: string;
onChangeValue: (value: string) => void;
placeholder: string;
className?: string;
disabled?: boolean;
compact?: boolean;
passwordBlur?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>) => {
@@ -168,7 +539,11 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam
onChange={(e) => onChangeValue(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
className={`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
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
rounded
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className}`}
style={{
@@ -412,6 +787,7 @@ export const VoidSwitch = ({
onChange,
size = 'md',
disabled = false,
...props
}: {
value: boolean;
onChange: (value: boolean) => void;
@@ -419,7 +795,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={`
@@ -635,7 +1011,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
onClick={() => setIsOpen(!isOpen)}
>
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
<span className={`truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
{getOptionDisplayName(selectedOption)}
</span>
<svg
@@ -657,57 +1033,60 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
{isOpen && (
<div
ref={refs.setFloating}
className="z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg"
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: matchInputWidth
width: (matchInputWidth
? (refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0)
: Math.max(
(refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0),
(measureRef.current instanceof HTMLElement ? measureRef.current.offsetWidth : 0)
),
))
}}
>
{options.map((option) => {
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
const optionName = getOptionDropdownName(option);
const optionDetail = getOptionDropdownDetail?.(option) || '';
onWheel={(e) => e.stopPropagation()}
><div className='overflow-auto max-h-80'>
return (
<div
key={optionName}
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
{options.map((option) => {
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
const optionName = getOptionDropdownName(option);
const optionDetail = getOptionDropdownDetail?.(option) || '';
return (
<div
key={optionName}
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
transition-all duration-100
bg-void-bg-1
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
`}
onClick={() => {
onChangeOption(option);
setIsOpen(false);
}}
>
<div className="w-4 flex justify-center flex-shrink-0">
{thisOptionIsSelected && (
<svg className="size-3" viewBox="0 0 12 12" fill="none">
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
onClick={() => {
onChangeOption(option);
setIsOpen(false);
}}
>
<div className="w-4 flex justify-center flex-shrink-0">
{thisOptionIsSelected && (
<svg className="size-3" viewBox="0 0 12 12" fill="none">
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
<span className="flex justify-between items-center w-full gap-x-1">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
</span>
</div>
<span className="flex justify-between w-full">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
</span>
</div>
);
})}
);
})}
</div>
</div>
)}
</div>
@@ -954,9 +1333,9 @@ export const BlockCode = ({ initValue, language, maxHeight, showScrollbars }: Bl
}
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: { children: React.ReactNode; disabled?: boolean; onClick: () => void; className?: string }) => {
return <button disabled={disabled}
className='px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap'
className={`px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap flex items-center justify-center ${className || ''}`}
onClick={onClick}
>{children}</button>
}
@@ -22,7 +22,6 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
@@ -47,6 +46,9 @@ import { IVoidModelService } from '../../../../common/voidModelService.js'
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
import { INativeHostService } from '../../../../../../../platform/native/common/native.js';
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
import { IToolsService } from '../../../toolsService.js'
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.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 +217,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
INativeHostService: accessor.get(INativeHostService),
IToolsService: accessor.get(IToolsService),
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
} as const
return reactAccessor
@@ -300,6 +304,16 @@ export const useChatThreadsStreamState = (threadId: string) => {
return s
}
export const useFullChatThreadsStreamState = () => {
const [s, ss] = useState(chatThreadsStreamState)
useEffect(() => {
ss(chatThreadsStreamState)
const listener = () => { ss(chatThreadsStreamState) }
chatThreadsStreamStateListeners.add(listener)
return () => { chatThreadsStreamStateListeners.delete(listener) }
}, [ss])
return s
}
@@ -350,9 +364,9 @@ export const useCommandBarURIListener = (listener: (uri: URI) => void) => {
export const useCommandBarState = () => {
const accessor = useAccessor()
const commandBarService = accessor.get('IVoidCommandBarService')
const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
const [s, ss] = useState({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
const listener = useCallback(() => {
ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
ss({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
}, [commandBarService])
useCommandBarURIListener(listener)
@@ -1,128 +1,130 @@
import { useEffect } from 'react';
// Get rid of this as it was causing lag
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
useEffect(() => {
if (!containerRef.current) return;
// import { useEffect } from 'react';
// Create selector for specific overflow classes
const overflowSelector = [
'[class*="overflow-auto"]',
'[class*="overflow-x-auto"]',
'[class*="overflow-y-auto"]'
].join(',');
// export const useScrollbarStyles = (containerRef: React.RefObject<HTMLDivElement | null>) => {
// useEffect(() => {
// if (!containerRef.current) return;
// Function to initialize scrollbar styles for elements
const initializeScrollbarStyles = () => {
// Get all matching elements within the container, including the container itself
const scrollElements = [
...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
];
// // Create selector for specific overflow classes
// const overflowSelector = [
// '[class*="overflow-auto"]',
// '[class*="overflow-x-auto"]',
// '[class*="overflow-y-auto"]'
// ].join(',');
// Apply basic styling to all elements
scrollElements.forEach(element => {
element.classList.add('void-scrollable-element');
});
// // Function to initialize scrollbar styles for elements
// const initializeScrollbarStyles = () => {
// // Get all matching elements within the container, including the container itself
// const scrollElements = [
// ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
// ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
// ];
// Only initialize fade effects for elements that haven't been initialized yet
scrollElements.forEach(element => {
if (!(element as any).__scrollbarCleanup) {
let fadeTimeout: NodeJS.Timeout | null = null;
let fadeInterval: NodeJS.Timeout | null = null;
// // Apply basic styling to all elements
// scrollElements.forEach(element => {
// element.classList.add('void-scrollable-element');
// });
const fadeIn = () => {
if (fadeInterval) clearInterval(fadeInterval);
// // Only initialize fade effects for elements that haven't been initialized yet
// scrollElements.forEach(element => {
// if (!(element as any).__scrollbarCleanup) {
// let fadeTimeout: NodeJS.Timeout | null = null;
// let fadeInterval: NodeJS.Timeout | null = null;
let step = 0;
fadeInterval = setInterval(() => {
if (step <= 10) {
element.classList.remove(`show-scrollbar-${step - 1}`);
element.classList.add(`show-scrollbar-${step}`);
step++;
} else {
clearInterval(fadeInterval!);
}
}, 10);
};
// const fadeIn = () => {
// if (fadeInterval) clearInterval(fadeInterval);
const fadeOut = () => {
if (fadeInterval) clearInterval(fadeInterval);
// let step = 0;
// fadeInterval = setInterval(() => {
// if (step <= 10) {
// element.classList.remove(`show-scrollbar-${step - 1}`);
// element.classList.add(`show-scrollbar-${step}`);
// step++;
// } else {
// clearInterval(fadeInterval!);
// }
// }, 10);
// };
let step = 10;
fadeInterval = setInterval(() => {
if (step >= 0) {
element.classList.remove(`show-scrollbar-${step + 1}`);
element.classList.add(`show-scrollbar-${step}`);
step--;
} else {
clearInterval(fadeInterval!);
}
}, 60);
};
// const fadeOut = () => {
// if (fadeInterval) clearInterval(fadeInterval);
const onMouseEnter = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
fadeIn();
};
// let step = 10;
// fadeInterval = setInterval(() => {
// if (step >= 0) {
// element.classList.remove(`show-scrollbar-${step + 1}`);
// element.classList.add(`show-scrollbar-${step}`);
// step--;
// } else {
// clearInterval(fadeInterval!);
// }
// }, 60);
// };
const onMouseLeave = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
fadeTimeout = setTimeout(() => {
fadeOut();
}, 10);
};
// const onMouseEnter = () => {
// if (fadeTimeout) clearTimeout(fadeTimeout);
// if (fadeInterval) clearInterval(fadeInterval);
// fadeIn();
// };
element.addEventListener('mouseenter', onMouseEnter);
element.addEventListener('mouseleave', onMouseLeave);
// const onMouseLeave = () => {
// if (fadeTimeout) clearTimeout(fadeTimeout);
// fadeTimeout = setTimeout(() => {
// fadeOut();
// }, 10);
// };
// Store cleanup function
const cleanup = () => {
element.removeEventListener('mouseenter', onMouseEnter);
element.removeEventListener('mouseleave', onMouseLeave);
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
element.classList.remove('void-scrollable-element');
// Remove any remaining show-scrollbar classes
for (let i = 0; i <= 10; i++) {
element.classList.remove(`show-scrollbar-${i}`);
}
};
// element.addEventListener('mouseenter', onMouseEnter);
// element.addEventListener('mouseleave', onMouseLeave);
// Store the cleanup function on the element for later use
(element as any).__scrollbarCleanup = cleanup;
}
});
};
// // Store cleanup function
// const cleanup = () => {
// element.removeEventListener('mouseenter', onMouseEnter);
// element.removeEventListener('mouseleave', onMouseLeave);
// if (fadeTimeout) clearTimeout(fadeTimeout);
// if (fadeInterval) clearInterval(fadeInterval);
// element.classList.remove('void-scrollable-element');
// // Remove any remaining show-scrollbar classes
// for (let i = 0; i <= 10; i++) {
// element.classList.remove(`show-scrollbar-${i}`);
// }
// };
// Initialize for the first time
initializeScrollbarStyles();
// // Store the cleanup function on the element for later use
// (element as any).__scrollbarCleanup = cleanup;
// }
// });
// };
// Set up mutation observer to do the same
const observer = new MutationObserver(() => {
initializeScrollbarStyles();
});
// // Initialize for the first time
// initializeScrollbarStyles();
// Start observing the container for child changes
observer.observe(containerRef.current, {
childList: true,
subtree: true
});
// // Set up mutation observer to do the same
// const observer = new MutationObserver(() => {
// initializeScrollbarStyles();
// });
return () => {
observer.disconnect();
// Your existing cleanup code...
if (containerRef.current) {
const scrollElements = [
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
];
scrollElements.forEach(element => {
if ((element as any).__scrollbarCleanup) {
(element as any).__scrollbarCleanup();
}
});
}
};
}, [containerRef]);
};
// // Start observing the container for child changes
// observer.observe(containerRef.current, {
// childList: true,
// subtree: true
// });
// return () => {
// observer.disconnect();
// // Your existing cleanup code...
// if (containerRef.current) {
// const scrollElements = [
// ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
// ...Array.from(containerRef.current.querySelectorAll(overflowSelector))
// ];
// scrollElements.forEach(element => {
// if ((element as any).__scrollbarCleanup) {
// (element as any).__scrollbarCleanup();
// }
// });
// }
// };
// }, [containerRef]);
// };
@@ -9,8 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
import '../styles.css'
import { useCallback, useEffect, useState, useRef } from 'react';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const isDark = useIsDark()
@@ -39,7 +40,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const commandService = accessor.get('ICommandService')
const commandBarService = accessor.get('IVoidCommandBarService')
const voidModelService = accessor.get('IVoidModelService')
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
// useEffect(() => {
@@ -110,7 +111,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const { model } = await voidModelService.getModelSafe(nextURI)
if (model) {
// switch to the URI
editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor)
editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor)
}
}
@@ -211,38 +212,47 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
if (!isADiffZoneInAnyFile) return null
const acceptAllButton = <button
className='text-nowrap'
// const acceptAllButton = <button
// className='text-nowrap'
// onClick={onAcceptAll}
// style={{
// backgroundColor: acceptAllBg,
// border: acceptBorder,
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Accept File
// </button>
// const rejectAllButton = <button
// className='text-nowrap'
// onClick={onRejectAll}
// style={{
// backgroundColor: rejectBg,
// border: rejectBorder,
// color: 'white',
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Reject File
// </button>
const acceptAllButton = <AcceptAllButtonWrapper
text={'Keep Changes'}
onClick={onAcceptAll}
style={{
backgroundColor: acceptAllBg,
border: acceptBorder,
color: buttonTextColor,
fontSize: buttonFontSize,
padding: '2px 4px',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Accept File
</button>
/>
const rejectAllButton = <button
className='text-nowrap'
const rejectAllButton = <RejectAllButtonWrapper
text={'Reject All'}
onClick={onRejectAll}
style={{
backgroundColor: rejectAllBg,
border: rejectBorder,
color: 'white',
fontSize: buttonFontSize,
padding: '2px 4px',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Reject File
</button>
/>
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
{acceptAllButton}
@@ -273,7 +283,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
{upButton}
{downButton}
<span className="min-w-16 px-2 text-xs">
<span className="min-w-16 px-2 text-xs leading-[1]">
{isADiffInThisFile ?
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming' ?
@@ -289,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
{rightButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
<span className="min-w-16 px-2 text-xs">
<span className="min-w-16 px-2 text-xs leading-[1]">
{currFileIdx !== null ?
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
@@ -299,7 +309,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
</div>
</div>
return <div className={`flex flex-col items-center gap-y-2 mx-2 pointer-events-auto`}>
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
{showAcceptRejectAll && acceptRejectAllButtons}
{leftRightUpDownButtons}
@@ -0,0 +1,170 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useAccessor, useActiveURI, useIsDark, useSettingsState } from '../util/services.js';
import '../styles.css'
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { Circle, MoreVertical } from 'lucide-react';
import { useEffect, useState } from 'react';
import { VoidSelectionHelperProps } from '../../../../../../contrib/void/browser/voidSelectionHelperWidget.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
export const VoidSelectionHelperMain = (props: VoidSelectionHelperProps) => {
const isDark = useIsDark()
return <div
className={`@@void-scope ${isDark ? 'dark' : ''}`}
>
<VoidSelectionHelper {...props} />
</div>
}
const VoidSelectionHelper = ({ rerenderKey }: VoidSelectionHelperProps) => {
const accessor = useAccessor()
const keybindingService = accessor.get('IKeybindingService')
const commandService = accessor.get('ICommandService')
const ctrlLKeybind = keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID)
const ctrlKKeybind = keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID)
const dividerHTML = <div className='w-[0.5px] bg-void-border-3'></div>
const [reactRerenderCount, setReactRerenderKey] = useState(rerenderKey)
const [clickState, setClickState] = useState<'init' | 'clickedOption' | 'clickedMore'>('init')
useEffect(() => {
const disposable = commandService.onWillExecuteCommand(e => {
if (e.commandId === VOID_CTRL_L_ACTION_ID || e.commandId === VOID_CTRL_K_ACTION_ID) {
setClickState('clickedOption')
}
});
return () => {
disposable.dispose();
};
}, [commandService, setClickState]);
// rerender when the key changes
if (reactRerenderCount !== rerenderKey) {
setReactRerenderKey(rerenderKey)
setClickState('init')
}
// useEffect(() => {
// }, [rerenderKey, reactRerenderCount, setReactRerenderKey, setClickState])
// if the user selected an option, close
if (clickState === 'clickedOption') {
return null
}
const defaultHTML = <>
{ctrlLKeybind &&
<div
className='
flex items-center px-2 py-1.5
cursor-pointer
'
onClick={() => {
commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
setClickState('clickedOption');
}}
>
<span>Add to Chat</span>
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
{ctrlLKeybind.getLabel()}
</span>
</div>
}
{ctrlLKeybind && ctrlKKeybind &&
dividerHTML
}
{ctrlKKeybind &&
<div
className='
flex items-center px-2 py-1.5
cursor-pointer
'
onClick={() => {
commandService.executeCommand(VOID_CTRL_K_ACTION_ID)
setClickState('clickedOption');
}}
>
<span className='ml-1'>Edit Inline</span>
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
{ctrlKKeybind.getLabel()}
</span>
</div>
}
{dividerHTML}
<div
className='
flex items-center px-0.5
cursor-pointer
'
onClick={() => {
setClickState('clickedMore');
}}
>
<MoreVertical className="w-4" />
</div>
</>
const moreOptionsHTML = <>
<div
className='
flex items-center px-2 py-1.5
cursor-pointer
'
onClick={() => {
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
setClickState('clickedOption');
}}
>
Disable Suggestions?
</div>
{dividerHTML}
<div
className='
flex items-center px-0.5
cursor-pointer
'
onClick={() => {
setClickState('init');
}}
>
<MoreVertical className="w-4" />
</div>
</>
return <div className='
pointer-events-auto select-none
z-[1000]
rounded-sm shadow-md flex flex-nowrap text-nowrap
border border-void-border-3 bg-void-bg-2
transition-all duration-200
'>
{clickState === 'init' ? defaultHTML
: clickState === 'clickedMore' ? moreOptionsHTML
: <></>
}
</div>
}
@@ -5,5 +5,9 @@
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { VoidCommandBarMain } from './VoidCommandBar.js'
import { VoidSelectionHelperMain } from './VoidSelectionHelper.js'
export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain)
export const mountVoidSelectionHelper = mountFnGenerator(VoidSelectionHelperMain)
@@ -0,0 +1,903 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useEffect, useRef, useState } from 'react';
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, 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';
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
const OVERRIDE_VALUE = false
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={`
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'}
`}
>
<ErrorBoundary>
<VoidOnboardingContent />
</ErrorBoundary>
</div>
</div>
)
}
const VoidIcon = () => {
const accessor = useAccessor()
const themeService = accessor.get('IThemeService')
const divRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
// void icon style
const updateTheme = () => {
const theme = themeService.getColorTheme().type
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
if (divRef.current) {
divRef.current.style.maxWidth = '220px'
divRef.current.style.opacity = '50%'
divRef.current.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
}
}
updateTheme()
const d = themeService.onDidColorThemeChange(updateTheme)
return () => d.dispose()
}, [])
return <div ref={divRef} className='@@void-void-icon' />
}
const FADE_DURATION_MS = 2000
const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { children: React.ReactNode, delayMs?: number, durationMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
const [opacity, setOpacity] = useState(0)
const effectiveDurationMs = durationMs ?? FADE_DURATION_MS
useEffect(() => {
const timeout = setTimeout(() => {
setOpacity(1)
}, delayMs)
return () => clearTimeout(timeout)
}, [setOpacity, delayMs])
return (
<div className={className} style={{ opacity, transition: `opacity ${effectiveDurationMs}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-zinc-100 enabled:hover:bg-zinc-100 disabled:bg-zinc-100/40 disabled:cursor-not-allowed rounded text-black duration-600 transition-all"
{...props.disabled && {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': 'Please enter all required fields or choose another provider',
'data-tooltip-place': 'top',
}}
{...props}
>
Next
</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-115 duration-600 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 text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
{bottom && <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 justify-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-emerald-500"
: val === false ? 'text-rose-600'
: "text text-amber-300"
}
>
{
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 } | undefined> = {}
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.type !== 'custom',
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] = {
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
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 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}${providerName}`} 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={!!true} /></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 flex items-center justify-center">{!!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">
<ErrorBoundary>
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true}
/>
</ErrorBoundary>
</td>
<td colSpan={4}></td>
</tr>
</tbody>
</table>
}
const PrimaryActionButton = ({ children, className, ringSize, ...props }: { children: React.ReactNode, ringSize?: undefined | 'xl' | 'screen' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
type='button'
className={`
flex items-center justify-center
text-white dark:text-black
bg-black/90 dark:bg-white/90
${ringSize === 'xl' ? `
gap-2 px-16 py-8
hover:ring-8 active:ring-8
transition-all duration-300 ease-in-out
`
: ringSize === 'screen' ? `
gap-2 px-16 py-8
ring-[3000px]
transition-all duration-1000 ease-in-out
`: ringSize === undefined ? `
gap-1 px-4 py-2
hover:ring-2 active:ring-2
transition-all duration-300 ease-in-out
`: ''}
hover:ring-black/90 dark:hover:ring-white/90
active:ring-black/90 dark:active:ring-white/90
rounded-lg
group
${className}
`}
{...props}
>
{children}
<ChevronRight
className={`
transition-all duration-300 ease-in-out
transform
group-hover:translate-x-1
group-active:translate-x-1
`}
/>
</button>
)
}
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')
// Replace the single selectedProviderName with four separate states
// page 2 state - each tab gets its own state
const [selectedIntelligentProvider, setSelectedIntelligentProvider] = useState<ProviderName>('anthropic');
const [selectedPrivateProvider, setSelectedPrivateProvider] = useState<ProviderName>('ollama');
const [selectedAffordableProvider, setSelectedAffordableProvider] = useState<ProviderName>('gemini');
const [selectedAllProvider, setSelectedAllProvider] = useState<ProviderName>('anthropic');
// Helper function to get the current selected provider based on active tab
const getSelectedProvider = (): ProviderName => {
switch (wantToUseOption) {
case 'smart': return selectedIntelligentProvider;
case 'private': return selectedPrivateProvider;
case 'cheap': return selectedAffordableProvider;
case 'all': return selectedAllProvider;
}
}
// Helper function to set the selected provider for the current tab
const setSelectedProvider = (provider: ProviderName) => {
switch (wantToUseOption) {
case 'smart': setSelectedIntelligentProvider(provider); break;
case 'private': setSelectedPrivateProvider(provider); break;
case 'cheap': setSelectedAffordableProvider(provider); break;
case 'all': setSelectedAllProvider(provider); break;
}
}
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio'],
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
all: providerNames,
}
const selectedProviderName = getSelectedProvider();
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-2">
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
<NextButton
onClick={() => { setPageIndex(pageIndex + 1) }}
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
/>
</div>
</div>
const lastPagePrevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
<div className="flex items-center gap-2">
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
<PrimaryActionButton
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
>Enter the Void</PrimaryActionButton>
</div>
</div>
// cannot be md
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Models with the best performance on benchmarks.",
private: "Host on your computer or local network for full data privacy.",
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: "Use great deals like Gemini 2.5 Pro, or self-host a model with Ollama or vLLM for free.",
all: "",
}
// Modified: initialize separate provider states on initial render instead of watching wantToUseOption changes
useEffect(() => {
if (selectedIntelligentProvider === undefined) {
setSelectedIntelligentProvider(providerNamesOfWantToUseOption['smart'][0]);
}
if (selectedPrivateProvider === undefined) {
setSelectedPrivateProvider(providerNamesOfWantToUseOption['private'][0]);
}
if (selectedAffordableProvider === undefined) {
setSelectedAffordableProvider(providerNamesOfWantToUseOption['cheap'][0]);
}
if (selectedAllProvider === undefined) {
setSelectedAllProvider(providerNamesOfWantToUseOption['all'][0]);
}
}, []);
// reset the page to page 0 if the user redos onboarding
useEffect(() => {
if (!voidSettingsState.globalSettings.isOnboardingComplete) {
setPageIndex(0)
}
}, [setPageIndex, voidSettingsState.globalSettings.isOnboardingComplete])
const contentOfIdx: { [pageIndex: number]: React.ReactNode } = {
0: <OnboardingPageShell
content={
<div className='flex flex-col items-center gap-8'>
<div className="text-5xl font-light text-center">Welcome to Void</div>
{/* Slice of Void image */}
<div className='max-w-md w-full h-[30vh] mx-auto flex items-center justify-center'>
<VoidIcon />
</div>
<FadeIn
delayMs={1000}
>
<PrimaryActionButton
onClick={() => { setPageIndex(pageIndex + 1) }}
>
Get Started
</PrimaryActionButton>
</FadeIn>
</div>
}
/>,
1: <OnboardingPageShell
hasMaxWidth={false}
top={<></>}
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
<button
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Brain size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
</button>
<button
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Lock size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Private</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
</button>
<button
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<DollarSign size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
</button>
</div>
</div>}
bottom={
<div className='mx-auto w-full max-w-[800px]'>
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
</div>
}
/>,
2: <OnboardingPageShell
top={
<>
{/* Title */}
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
{/* Preference Selector */}
<div
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
>
{[
{ id: 'smart', label: 'Intelligent' },
{ id: 'private', label: 'Private' },
{ id: 'cheap', label: 'Affordable' },
{ id: 'all', label: 'All' }
].map(option => (
<ErrorBoundary
key={option.id}
>
<button
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
</ErrorBoundary>
))}
</div>
{/* Provider Buttons - Modified to use separate components for each tab */}
<div className="mb-2 w-full">
{/* Intelligent tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Private tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Affordable tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* All tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
</div>
{/* Description */}
<ErrorBoundary>
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
</ErrorBoundary>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
{/* Models Table */}
<ErrorBoundary>
<TableOfModelsForProvider providerName={selectedProviderName} />
</ErrorBoundary>
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8 mx-auto'>
<ErrorBoundary>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
<div className='my-4'>
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
</div>
</div>
</ErrorBoundary>
<ErrorBoundary>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
</ErrorBoundary>
{/* Button and status indicators */}
<ErrorBoundary>
{!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>
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
</ErrorBoundary>
</div>
</div>}
</>
}
bottom={
<ErrorBoundary>
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
</ErrorBoundary>
}
/>,
// 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
content={
<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={lastPagePrevAndNextButtons}
// bottom={prevAndNextButtons}
/>,
// 4: <OnboardingPageShell
// content={
// <>
// <div
// className='flex justify-center'
// >
// <PrimaryActionButton
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
// className='text-4xl'
// >Enter the Void</PrimaryActionButton>
// </div>
// </>
// }
// bottom={
// <PreviousButton
// onClick={() => { setPageIndex(pageIndex - 1) }}
// />
// }
// />,
}
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
<ErrorBoundary>
{contentOfIdx[pageIndex]}
</ErrorBoundary>
</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)
@@ -12,6 +12,7 @@ import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
import { modelFilterOfFeatureName, ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { WarningBox } from './WarningBox.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
if (m1.length !== m2.length) return false
@@ -44,37 +45,6 @@ const ModelSelectBox = ({ options, featureName, className }: { options: ModelOpt
matchInputWidth={false}
/>
}
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
// let weChangedText = false
// return <VoidSelectBox
// className='@@[&_select]:!void-text-xs text-void-fg-3'
// options={options}
// onChangeSelection={useCallback((newVal: ModelSelection) => {
// if (weChangedText) return
// voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
// }, [voidSettingsService, featureName])}
// // we are responsible for setting the initial state here. always sync instance when state changes.
// onCreateInstance={useCallback((instance: SelectBox) => {
// const syncInstance = () => {
// const modelsListRef = voidSettingsService.state._modelOptions // as a ref
// const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
// const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
// weChangedText = true
// instance.select(selectionIdx === -1 ? 0 : selectionIdx)
// weChangedText = false
// }
// syncInstance()
// const disposable = voidSettingsService.onDidChangeState(syncInstance)
// return [disposable]
// }, [voidSettingsService, featureName])}
// />
// }
const MemoizedModelDropdown = ({ featureName, className }: { featureName: FeatureName, className: string }) => {
@@ -123,5 +93,7 @@ export const ModelDropdown = ({ featureName, className }: { featureName: Feature
: 'Provider required'
} />
return <MemoizedModelDropdown featureName={featureName} className={className} />
return <ErrorBoundary>
<MemoizedModelDropdown featureName={featureName} className={className} />
</ErrorBoundary>
}
File diff suppressed because it is too large Load Diff
@@ -15,7 +15,7 @@ export const WarningBox = ({ text, onClick, className }: { text: string; onClick
>
<IconWarning
size={14}
className='mr-1'
className='mr-1 flex-shrink-0'
/>
<span>{text}</span>
</div>
@@ -0,0 +1,125 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import '../styles.css'
import { Tooltip } from 'react-tooltip';
import 'react-tooltip/dist/react-tooltip.css';
import { useIsDark } from '../util/services.js';
/**
* Creates a configured global tooltip component with consistent styling
* To use:
* 1. Mount a Tooltip with some id eg id='void-tooltip'
* 2. Add data-tooltip-id="void-tooltip" and data-tooltip-content="Your tooltip text" to any element
*/
export const VoidTooltip = () => {
const isDark = useIsDark()
return (
// use native colors so we don't have to worry about @@void-scope styles
// --void-bg-1: var(--vscode-input-background);
// --void-bg-1-alt: var(--vscode-badge-background);
// --void-bg-2: var(--vscode-sideBar-background);
// --void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
// --void-bg-3: var(--vscode-editor-background);
// --void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
// --void-fg-1: var(--vscode-editor-foreground);
// --void-fg-2: var(--vscode-input-foreground);
// --void-fg-3: var(--vscode-input-placeholderForeground);
// /* --void-fg-4: var(--vscode-tab-inactiveForeground); */
// --void-fg-4: var(--vscode-list-deemphasizedForeground);
// --void-warning: var(--vscode-charts-yellow);
// --void-border-1: var(--vscode-commandCenter-activeBorder);
// --void-border-2: var(--vscode-commandCenter-border);
// --void-border-3: var(--vscode-commandCenter-inactiveBorder);
// --void-border-4: var(--vscode-editorGroup-border);
<>
<style>
{`
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings {
font-size: 12px;
padding: 0px 8px;
border-radius: 6px;
z-index: 999999;
}
#void-tooltip {
background-color: var(--vscode-editor-background);
color: var(--vscode-input-foreground);
}
#void-tooltip-orange {
background-color: #F6762A;
color: white;
}
#void-tooltip-green {
background-color: #228B22;
color: white;
}
#void-tooltip-ollama-settings {
background-color: var(--vscode-editor-background);
color: var(--vscode-input-foreground);
}
.react-tooltip-arrow {
z-index: -1 !important; /* Keep arrow behind content (somehow this isnt done automatically) */
}
`}
</style>
<Tooltip
id="void-tooltip"
// border='1px solid var(--vscode-editorGroup-border)'
border='1px solid rgba(100,100,100,.2)'
opacity={1}
delayShow={50}
/>
<Tooltip
id="void-tooltip-orange"
border='1px solid rgba(200,200,200,.3)'
opacity={1}
delayShow={50}
/>
<Tooltip
id="void-tooltip-green"
border='1px solid rgba(200,200,200,.3)'
opacity={1}
delayShow={50}
/>
<Tooltip
id="void-tooltip-ollama-settings"
border='1px solid rgba(100,100,100,.2)'
opacity={1}
openEvents={{ mouseover: true, click: true, focus: true }}
place='right'
style={{ pointerEvents: 'all', userSelect: 'text', fontSize: 11 }}
>
<div style={{ padding: '8px 10px' }}>
<div style={{ opacity: 0.8, textAlign: 'center', fontWeight: 'bold', marginBottom: 8 }}>
Good starter models
</div>
<div style={{ marginBottom: 4 }}>
<span style={{ opacity: 0.8 }}>For chat:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>llama3.1</span>
</div>
<div>
<span style={{ opacity: 0.8 }}>For autocomplete:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder:1.5b</span>
</div>
</div>
</Tooltip>
</>
);
};
@@ -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 { VoidTooltip } from './VoidTooltip.js'
export const mountVoidTooltip = mountFnGenerator(VoidTooltip)
@@ -53,6 +53,7 @@ module.exports = {
'void-bg-1-alt': 'var(--void-bg-1-alt)',
'void-bg-2': 'var(--void-bg-2)',
'void-bg-2-alt': 'var(--void-bg-2-alt)',
'void-bg-2-hover': 'var(--void-bg-2-hover)',
'void-bg-3': 'var(--void-bg-3)',
@@ -7,9 +7,11 @@ import { defineConfig } from 'tsup'
export default defineConfig({
entry: [
'./src2/void-command-bar-tsx/index.tsx',
'./src2/void-editor-widgets-tsx/index.tsx',
'./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);
@@ -14,7 +14,6 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { VOID_VIEW_ID } from './sidebarPane.js';
import { IMetricsService } from '../common/metricsService.js';
import { ISidebarStateService } from './sidebarStateService.js';
@@ -26,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
@@ -53,24 +79,17 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e
return newRange
}
const getContentInRange = (model: ITextModel, range: IRange | null) => {
if (!range)
return null
const content = model.getValueInRange(range)
const trimmedContent = content
.replace(/^\s*\n/g, '') // trim pure whitespace lines from start
.replace(/\n\s*$/g, '') // trim pure whitespace lines from end
return trimmedContent
}
// const getContentInRange = (model: ITextModel, range: IRange | null) => {
// if (!range)
// return null
// const content = model.getValueInRange(range)
// const trimmedContent = content
// .replace(/^\s*\n/g, '') // trim pure whitespace lines from start
// .replace(/\n\s*$/g, '') // trim pure whitespace lines from end
// return trimmedContent
// }
const findMatchingStagingIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem) => {
return currentSelections?.findIndex(s =>
s.fileURI.fsPath === newSelection.fileURI.fsPath
&& s.range?.startLineNumber === newSelection.range?.startLineNumber
&& s.range?.endLineNumber === newSelection.range?.endLineNumber
)
}
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
registerAction2(class extends Action2 {
@@ -114,22 +133,18 @@ registerAction2(class extends Action2 {
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
}
const selectionStr = getContentInRange(model, selectionRange)
const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
fileURI: model.uri,
uri: model.uri,
language: model.getLanguageId(),
selectionStr: null,
range: null,
state: { isOpened: false, wasAddedAsCurrentFile: false }
state: { wasAddedAsCurrentFile: false }
} : {
type: 'Selection',
fileURI: model.uri,
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
selectionStr: selectionStr,
range: selectionRange,
state: { isOpened: true, wasAddedAsCurrentFile: false }
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false }
}
// update the staging selections
@@ -149,21 +164,18 @@ registerAction2(class extends Action2 {
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
}
// close all selections besides the new one
selections = selections.map(s => ({ ...s, state: { ...s.state, isOpened: false } }))
// if matches with existing selection, overwrite (since text may change)
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
const idx = findStagingSelectionIndex(selections, newSelection)
if (idx !== null && idx !== -1) {
setSelections([
...selections!.slice(0, matchingStagingEltIdx),
selection,
...selections!.slice(matchingStagingEltIdx + 1, Infinity)
...selections!.slice(0, idx),
newSelection,
...selections!.slice(idx + 1, Infinity)
])
}
// if no match, add it
else {
setSelections([...(selections ?? []), selection])
setSelections([...(selections ?? []), newSelection])
}
}
@@ -175,7 +187,7 @@ registerAction2(class extends Action2 {
super({
id: VOID_CTRL_L_ACTION_ID,
f1: true,
title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension
@@ -190,7 +202,19 @@ registerAction2(class extends Action2 {
})
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
// focus
stateService.fireFocusChat()
const window = getActiveWindow()
window.requestAnimationFrame(() => stateService.fireFocusChat())
}
// New chat menu button
@@ -200,19 +224,43 @@ registerAction2(class extends Action2 {
id: 'void.newChatAction',
title: 'New Chat',
icon: { id: 'add' },
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Chat Navigation', { type: 'New Chat' })
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
openNewThreadAndFireFocus(accessor)
}
})
// New chat keybind
registerAction2(class extends Action2 {
constructor() {
super({
id: 'void.newChatKeybindAction',
title: 'New Chat Keybind',
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension,
},
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const metricsService = accessor.get(IMetricsService)
const commandService = accessor.get(ICommandService)
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
openNewThreadAndFireFocus(accessor)
// add user's selection to chat
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
}
})
@@ -227,13 +275,27 @@ registerAction2(class extends Action2 {
});
}
async run(accessor: ServicesAccessor): Promise<void> {
// do not do anything if there are no messages (without this it clears all of the user's selections if the button is pressed)
// TODO the history button should be disabled in this case so we can remove this logic
const thread = accessor.get(IChatThreadService).getCurrentThread()
if (thread.messages.length === 0) {
return;
}
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Chat Navigation', { type: 'History' })
openNewThreadAndFireFocus(accessor)
// doesnt do anything right now
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
stateService.fireBlurChat()
}
})
@@ -108,7 +108,7 @@ export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
const container = viewContainerRegistry.registerViewContainer({
id: VOID_VIEW_CONTAINER_ID,
title: nls.localize2('voidContainer', 'Void Chat'), // this is used to say "Void" (Ctrl + L)
title: nls.localize2('voidContainer', 'Chat'), // this is used to say "Void" (Ctrl + L)
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, {
mergeViewWithContainerWhenSingleView: true,
orientation: Orientation.HORIZONTAL,
@@ -13,7 +13,7 @@ import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
// service that manages sidebar's state
export type VoidSidebarState = {
isHistoryOpen: boolean;
isHistoryOpen: boolean; // this isn't doing anything right now
currentTab: 'chat';
}
@@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { TerminalExitReason, TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js';
@@ -18,9 +18,12 @@ export interface ITerminalToolService {
readonly _serviceBrand: undefined;
listTerminalIds(): string[];
runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>;
openTerminal(terminalId: string): Promise<void>
runCommand(command: string, bgTerminalId: string | null): Promise<{ terminalId: string, resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
focusTerminal(terminalId: string): Promise<void>
terminalExists(terminalId: string): boolean
createTerminal(): Promise<string>
killTerminal(terminalId: string): Promise<void>
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
@@ -103,12 +106,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
throw new Error('This should never be reached by pigeonhole principle');
}
private async _getOrCreateTerminal(proposedTerminalId: string) {
// if terminal ID exists, return it
if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false }
async createTerminal() {
// create new terminal and return its ID
const terminalId = this.getValidNewTerminalId();
const terminal = await this.terminalService.createTerminal({
@@ -127,13 +125,21 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
})
disposables.push(d)
})
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 1000) })
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
await Promise.any([waitForMount, waitForTimeout,])
disposables.forEach(d => d.dispose())
this.terminalInstanceOfId[terminalId] = terminal
return { terminalId, didCreateTerminal: true }
return terminalId
}
async killTerminal(terminalId: string) {
const terminal = this.terminalInstanceOfId[terminalId]
if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} did not exist.`);
terminal.dispose(TerminalExitReason.Extension)
delete this.terminalInstanceOfId[terminalId]
return
}
terminalExists(terminalId: string): boolean {
@@ -141,7 +147,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
openTerminal: ITerminalToolService['openTerminal'] = async (terminalId) => {
focusTerminal: ITerminalToolService['focusTerminal'] = async (terminalId) => {
if (!terminalId) return
const terminal = this.terminalInstanceOfId[terminalId]
if (!terminal) return // should never happen
@@ -151,81 +157,104 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => {
runCommand: ITerminalToolService['runCommand'] = async (command, bgTerminalId) => {
await this.terminalService.whenConnected;
const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId)
const terminal = this.terminalInstanceOfId[terminalId];
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`);
// focus the terminal about to run
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
let result: string = ''
let resolveReason: TerminalResolveReason | undefined = undefined
let terminal: ITerminalInstance
const disposables: IDisposable[] = []
const waitUntilDone = new Promise<void>((res, rej) => {
const d2 = terminal.onData(async newData => {
if (resolveReason) return
const isBG = bgTerminalId !== null
let terminalId: string
if (isBG) { // BG process
terminal = this.terminalInstanceOfId[bgTerminalId];
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${bgTerminalId} did not exist.`);
terminalId = bgTerminalId
}
else {
terminalId = await this.createTerminal()
terminal = this.terminalInstanceOfId[terminalId]
if (!terminal) throw new Error(`Unexpected error: Terminal could not be created.`)
}
result += newData
// onPageFull
if (result.length > MAX_TERMINAL_CHARS_PAGE) {
result = result.substring(0, MAX_TERMINAL_CHARS_PAGE)
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
resolveReason = { type: 'toofull' }
res()
return
}
const waitForResult = async () => {
// focus the terminal about to run
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
// onDone
const isDone = isCommandComplete(result)
if (isDone) {
resolveReason = { type: 'done', exitCode: isDone.exitCode }
res()
return
}
let result: string = ''
let resolveReason: TerminalResolveReason | undefined = undefined
// create this before we send so that we don't miss events on terminal
const waitUntilDone = new Promise<void>((res, rej) => {
const d2 = terminal.onData(async newData => {
if (resolveReason) return
result += newData
// onDone
const isDone = isCommandComplete(result)
if (isDone) {
resolveReason = { type: 'done', exitCode: isDone.exitCode }
res()
return
}
})
disposables.push(d2)
})
disposables.push(d2)
})
// send the command here
await terminal.sendText(command, true)
// send the command here
await terminal.sendText(command, true)
// timeout promise
const waitUntilTimeout = new Promise<void>((res, rej) => {
setTimeout(async () => {
if (resolveReason) return
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
resolveReason = { type: waitForCompletion ? 'timeout' : 'bgtask' }
res()
return
}, (waitForCompletion ? TERMINAL_TIMEOUT_TIME : TERMINAL_BG_WAIT_TIME) * 1000)
})
// inactivity-based timeout
const waitUntilInactive = new Promise<void>(res => {
let globalTimeoutId: ReturnType<typeof setTimeout>;
const resetTimer = () => {
clearTimeout(globalTimeoutId);
globalTimeoutId = setTimeout(() => {
if (resolveReason) return
await Promise.any([
waitUntilDone,
waitUntilTimeout,
])
resolveReason = { type: 'timeout' };
res();
}, MAX_TERMINAL_INACTIVE_TIME * 1000);
};
disposables.forEach(d => d.dispose())
const dTimeout = terminal.onData(() => { resetTimer(); });
disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId)));
resetTimer();
});
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
// wait for result
await Promise.any([waitUntilDone, waitUntilInactive,])
disposables.forEach(d => d.dispose())
if (!isBG) {
await this.killTerminal(terminalId)
}
result = removeAnsiEscapeCodes(result)
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
.join('\n')
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
return { terminalId, didCreateTerminal, result, resolveReason }
result = removeAnsiEscapeCodes(result)
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
.join('\n')
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2
result = result.slice(0, half)
+ '\n...\n'
+ result.slice(result.length - half, Infinity)
}
return { result, resolveReason }
}
const resPromise = waitForResult()
return { terminalId, resPromise }
}
}
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed);
@@ -8,11 +8,17 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
import { ISearchService } from '../../../services/search/common/search.js'
import { IEditCodeService } from './editCodeServiceInterface.js'
import { ITerminalToolService } from './terminalToolService.js'
import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
import { IVoidModelService } from '../common/voidModelService.js'
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, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
import { IVoidSettingsService } from '../common/voidSettingsService.js'
// tool use for AI
@@ -20,119 +26,41 @@ import { IVoidCommandBarService } from './voidCommandBarService.js'
type ValidateParams = { [T in ToolName]: (p: string) => Promise<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: ToolResultType[T]) => string }
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
// pagination info
const MAX_FILE_CHARS_PAGE = 50_000
const MAX_CHILDREN_URIs_PAGE = 500
export const MAX_TERMINAL_CHARS_PAGE = 20_000
export const TERMINAL_TIMEOUT_TIME = 15
export const TERMINAL_BG_WAIT_TIME = 1
const computeDirectoryResult = async (
fileService: IFileService,
rootURI: URI,
pageNumber: number = 1
): Promise<ToolResultType['list_dir']> => {
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
if (!stat.isDirectory) {
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
}
const originalChildrenLength = stat.children?.length ?? 0;
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? [];
const children: ToolDirectoryItem[] = listChildren.map(child => ({
name: child.name,
uri: child.resource,
isDirectory: child.isDirectory,
isSymbolicLink: child.isSymbolicLink
}));
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
const hasPrevPage = pageNumber > 1;
const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1));
return {
children,
hasNextPage,
hasPrevPage,
itemsRemaining
};
};
const directoryResultToString = (params: ToolCallParams['list_dir'], result: ToolResultType['list_dir']): string => {
if (!result.children) {
return `Error: ${params.rootURI} is not a directory`;
}
let output = '';
const entries = result.children;
if (!result.hasPrevPage) { // is first page
output += `${params.rootURI.fsPath}\n`;
}
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const isLast = i === entries.length - 1 && !result.hasNextPage;
const prefix = isLast ? '└── ' : '├── ';
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
}
if (result.hasNextPage) {
output += `└── (${result.itemsRemaining} results remaining...)\n`;
}
return output;
};
const validateJSON = (s: string): { [s: string]: unknown } => {
try {
const o = JSON.parse(s)
if (typeof o !== 'object') throw new Error()
if ('result' in o) { // openrouter sometimes wraps the result with { 'result': ... }
return o.result
}
return o
}
catch (e) {
throw new Error(`Invalid LLM output format: Tool parameter was not a string of a valid JSON: "${s}".`)
}
const isFalsy = (u: unknown) => {
return !u || u === 'null' || u === 'undefined'
}
const validateStr = (argName: string, value: unknown) => {
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`)
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a ${typeof value}. Value: ${value}.`)
return value
}
// We are NOT checking to make sure in workspace
// TODO!!!! check to make sure folder/file exists
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.')
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a ${typeof uriStr}. Value: ${uriStr}.`)
const uri = URI.file(uriStr)
return uri
}
const validateOptionalURI = (uriStr: unknown) => {
if (isFalsy(uriStr)) return null
return validateURI(uriStr)
}
const validateOptionalStr = (argName: string, str: unknown) => {
if (isFalsy(str)) return null
return validateStr(argName, str)
}
const validatePageNum = (pageNumberUnknown: unknown) => {
if (!pageNumberUnknown) return 1
const parsedInt = Number.parseInt(pageNumberUnknown + '')
@@ -141,7 +69,22 @@ const validatePageNum = (pageNumberUnknown: unknown) => {
return parsedInt
}
const validateNumber = (numStr: unknown, opts: { default: number | null }) => {
if (typeof numStr === 'number')
return numStr
if (isFalsy(numStr)) return opts.default
if (typeof numStr === 'string') {
const parsedInt = Number.parseInt(numStr + '')
if (!Number.isInteger(parsedInt)) return opts.default
return parsedInt
}
return opts.default
}
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
if (!paramsUnknown) return false
if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.')
const params = paramsUnknown
const isRecursive = params.includes('r')
@@ -154,12 +97,15 @@ const validateProposedTerminalId = (terminalIdUnknown: unknown) => {
return terminalId
}
const validateWaitForCompletion = (b: unknown) => {
const validateBoolean = (b: unknown, opts: { default: boolean }) => {
if (typeof b === 'string') {
if (b === 'true') return true
if (b === 'false') return false
}
return true // default is true
if (typeof b === 'boolean') {
return b
}
return opts.default
}
@@ -195,62 +141,99 @@ export class ToolsService implements IToolsService {
@IEditCodeService editCodeService: IEditCodeService,
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
@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: string) => {
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
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)
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: (params: RawToolParamsObj) => {
const { uri: uriStr, page_number: pageNumberUnknown } = params
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
return { uri, pageNumber }
},
list_dir: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
get_dir_tree: (params: RawToolParamsObj) => {
const { uri: uriStr, } = params
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
return { rootURI: uri, pageNumber }
return { uri }
},
pathname_search: async (params: string) => {
const o = validateJSON(params)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
search_pathnames_only: (params: RawToolParamsObj) => {
const {
query: queryUnknown,
search_in_folder: includeUnknown,
page_number: pageNumberUnknown
} = params
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const includePattern = validateOptionalStr('include_pattern', includeUnknown)
return { queryStr, pageNumber }
return { query: queryStr, includePattern, pageNumber }
},
grep_search: async (params: string) => {
const o = validateJSON(params)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
search_for_files: (params: RawToolParamsObj) => {
const {
query: queryUnknown,
search_in_folder: searchInFolderUnknown,
is_regex: isRegexUnknown,
page_number: pageNumberUnknown
} = params
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const searchInFolder = validateOptionalURI(searchInFolderUnknown)
const isRegex = validateBoolean(isRegexUnknown, { default: false })
return {
query: queryStr,
isRegex,
searchInFolder,
pageNumber
}
},
search_in_file: (params: RawToolParamsObj) => {
const { uri: uriStr, query: queryUnknown, is_regex: isRegexUnknown } = params;
const uri = validateURI(uriStr);
const query = validateStr('query', queryUnknown);
const isRegex = validateBoolean(isRegexUnknown, { default: false });
return { uri, query, isRegex };
},
return { queryStr, pageNumber }
read_lint_errors: (params: RawToolParamsObj) => {
const {
uri: uriUnknown,
} = params
const uri = validateURI(uriUnknown)
return { uri }
},
// ---
create_uri: async (params: string) => {
const o = validateJSON(params)
const { uri: uriUnknown } = o
create_file_or_folder: (params: RawToolParamsObj) => {
const { uri: uriUnknown } = params
const uri = validateURI(uriUnknown)
const uriStr = validateStr('uri', uriUnknown)
const isFolder = checkIfIsFolder(uriStr)
return { uri, isFolder }
},
delete_uri: async (params: string) => {
const o = validateJSON(params)
const { uri: uriUnknown, params: paramsStr } = o
delete_file_or_folder: (params: RawToolParamsObj) => {
const { uri: uriUnknown, params: paramsStr } = params
const uri = validateURI(uriUnknown)
const isRecursive = validateRecursiveParamStr(paramsStr)
const uriStr = validateStr('uri', uriUnknown)
@@ -258,49 +241,73 @@ export class ToolsService implements IToolsService {
return { uri, isRecursive, isFolder }
},
edit: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
edit_file: (params: RawToolParamsObj) => {
const { uri: uriStr, change_diff: changeDiffUnknown } = params
const uri = validateURI(uriStr)
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
return { uri, changeDescription }
const changeDiff = validateStr('changeDiff', changeDiffUnknown)
return { uri, changeDiff }
},
terminal_command: async (s: string) => {
const o = validateJSON(s)
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
const command = validateStr('command', commandUnknown)
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown)
return { command, proposedTerminalId, waitForCompletion }
// ---
run_command: (params: RawToolParamsObj) => {
const { command: commandUnknown, terminal_id: terminalIdUnknown } = params;
const command = validateStr('command', commandUnknown);
const proposedTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null;
return { command, bgTerminalId: proposedTerminalId };
},
open_persistent_terminal: (_params: RawToolParamsObj) => {
// No parameters needed; will open a new background terminal
return {};
},
kill_persistent_terminal: (params: RawToolParamsObj) => {
const { terminal_id: terminalIdUnknown } = params;
const terminalId = validateProposedTerminalId(terminalIdUnknown);
return { terminalId };
},
}
this.callTool = {
read_file: async ({ uri, pageNumber }) => {
read_file: async ({ uri, startLine, endLine, pageNumber }) => {
await voidModelService.initializeModel(uri)
const { model } = await voidModelService.getModelSafe(uri)
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
const readFileContents = model.getValue(EndOfLinePreference.LF)
if (model === null) { throw new Error(`No contents; File does not exist.`) }
let contents: string
if (startLine === null && endLine === null) {
contents = model.getValue(EndOfLinePreference.LF)
}
else {
const startLineNumber = startLine === null ? 1 : startLine
const endLineNumber = endLine === null ? model.getLineCount() : endLine
contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
}
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
return { result: { fileContents, hasNextPage } }
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (contents.length - 1) - toIdx >= 1
const totalFileLen = contents.length
return { result: { fileContents, totalFileLen, hasNextPage } }
},
list_dir: async ({ rootURI, pageNumber }) => {
const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber)
ls_dir: async ({ uri, pageNumber }) => {
const dirResult = await computeDirectoryTree1Deep(fileService, uri, pageNumber)
return { result: dirResult }
},
pathname_search: async ({ queryStr, pageNumber }) => {
get_dir_tree: async ({ uri }) => {
const str = await this.directoryStrService.getDirectoryStrTool(uri)
return { result: { str } }
},
search_pathnames_only: async ({ query: queryStr, includePattern, pageNumber }) => {
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
filePattern: queryStr,
includePattern: includePattern ?? undefined,
})
const data = await searchService.fileSearch(query, CancellationToken.None)
@@ -314,11 +321,15 @@ export class ToolsService implements IToolsService {
return { result: { uris, hasNextPage } }
},
grep_search: async ({ queryStr, pageNumber }) => {
search_for_files: async ({ query: queryStr, isRegex, searchInFolder, pageNumber }) => {
const searchFolders = searchInFolder === null ?
workspaceContextService.getWorkspace().folders.map(f => f.uri)
: [searchInFolder]
const query = queryBuilder.text({
pattern: queryStr,
isRegExp: true,
}, workspaceContextService.getWorkspace().folders.map(f => f.uri))
isRegExp: isRegex,
}, searchFolders)
const data = await searchService.textSearch(query, CancellationToken.None)
@@ -331,10 +342,34 @@ export class ToolsService implements IToolsService {
const hasNextPage = (data.results.length - 1) - toIdx >= 1
return { result: { queryStr, uris, hasNextPage } }
},
search_in_file: async ({ uri, query, isRegex }) => {
await voidModelService.initializeModel(uri);
const { model } = await voidModelService.getModelSafe(uri);
if (model === null) { throw new Error(`No contents; File does not exist.`); }
const contents = model.getValue(EndOfLinePreference.LF);
const contentOfLine = contents.split('\n');
const totalLines = contentOfLine.length;
const regex = isRegex ? new RegExp(query) : null;
const lines: number[] = []
for (let i = 0; i < totalLines; i++) {
const line = contentOfLine[i];
if ((isRegex && regex!.test(line)) || (!isRegex && line.includes(query))) {
const matchLine = i + 1;
lines.push(matchLine);
}
}
return { result: { lines } };
},
read_lint_errors: async ({ uri }) => {
await timeout(1000)
const { lintErrors } = this._getLintErrors(uri)
return { result: { lintErrors } }
},
// ---
create_uri: async ({ uri, isFolder }) => {
create_file_or_folder: async ({ uri, isFolder }) => {
if (isFolder)
await fileService.createFolder(uri)
else {
@@ -343,90 +378,155 @@ export class ToolsService implements IToolsService {
return { result: {} }
},
delete_uri: async ({ uri, isRecursive }) => {
delete_file_or_folder: async ({ uri, isRecursive }) => {
await fileService.del(uri, { recursive: isRecursive })
return { result: {} }
},
edit: async ({ uri, changeDescription }) => {
edit_file: async ({ uri, changeDiff }) => {
await voidModelService.initializeModel(uri)
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`)
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
}
const res = await editCodeService.startApplying({
const opts = {
uri,
applyStr: changeDescription,
applyStr: changeDiff,
from: 'ClickApply',
startBehavior: 'keep-conflicts',
})
} as const
await editCodeService.callBeforeStartApplying(opts)
const res = editCodeService.startApplying(opts)
if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`)
const [diffZoneURI, applyDonePromise] = res
const interruptTool = () => { // must reject the applyPromiseDone promise
editCodeService.interruptURIStreaming({ uri: diffZoneURI })
}
return { result: applyDonePromise, interruptTool }
// at end, get lint errors
const lintErrorsPromise = applyDonePromise.then(async () => {
await timeout(2000)
const { lintErrors } = this._getLintErrors(uri)
return { lintErrors }
})
return { result: lintErrorsPromise, interruptTool }
},
terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
return { result: { terminalId, didCreateTerminal, result, resolveReason } }
// ---
run_command: async ({ command, bgTerminalId }) => {
const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, bgTerminalId)
const interruptTool = () => {
this.terminalToolService.killTerminal(terminalId)
}
return { result: resPromise, interruptTool }
},
open_persistent_terminal: async () => {
// Open a new background terminal without waiting for completion
const terminalId = await this.terminalToolService.createTerminal()
return { result: { terminalId } }
},
kill_persistent_terminal: async ({ terminalId }) => {
// Close the background terminal by sending exit
await this.terminalToolService.killTerminal(terminalId)
return { result: {} }
},
}
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
// given to the LLM after the call
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')
.substring(0, MAX_FILE_CHARS_PAGE)
}
// given to the LLM after the call for successful tool calls
this.stringOfResult = {
read_file: (params, result) => {
return result.fileContents + nextPageStr(result.hasNextPage)
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
},
list_dir: (params, result) => {
const dirTreeStr = directoryResultToString(params, result)
ls_dir: (params, result) => {
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining
},
pathname_search: (params, result) => {
get_dir_tree: (params, result) => {
return result.str
},
search_pathnames_only: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
grep_search: (params, result) => {
search_for_files: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
search_in_file: (params, result) => {
const { model } = voidModelService.getModel(params.uri)
if (!model) return '<Error getting string of result>'
const lines = result.lines.map(n => {
const lineContent = model.getValueInRange({ startLineNumber: n, startColumn: 1, endLineNumber: n, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
return `Line ${n}:\n\`\`\`\n${lineContent}\n\`\`\``
}).join('\n\n');
return lines;
},
read_lint_errors: (params, result) => {
return result.lintErrors ?
stringifyLintErrors(result.lintErrors)
: 'No lint errors found.'
},
// ---
create_uri: (params, result) => {
create_file_or_folder: (params, result) => {
return `URI ${params.uri.fsPath} successfully created.`
},
delete_uri: (params, result) => {
delete_file_or_folder: (params, result) => {
return `URI ${params.uri.fsPath} successfully deleted.`
},
edit: (params, result) => {
console.log('STR OF RESULT', params)
return `Change successfully made to ${params.uri.fsPath}.`
},
terminal_command: (params, result) => {
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.`)
: '')
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
},
run_command: (params, result) => {
const {
terminalId,
didCreateTerminal,
resolveReason,
result: result_,
} = result
const { bgTerminalId } = params
const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}`
// success
if (resolveReason.type === 'done') {
const desc = bgTerminalId ? ` in terminal ${bgTerminalId}` : ''
return `Terminal command executed and finished${desc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
}
if (resolveReason.type === 'timeout') {
return `Terminal command ran in ${terminalDesc}, but timed out after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
// bg command
if (bgTerminalId !== null) {
if (resolveReason.type === 'timeout') {
return `Terminal command is running in the background in terminal ${bgTerminalId}. Here were the outputs after ${MAX_TERMINAL_INACTIVE_TIME} seconds:\n${result_}`
}
}
else if (resolveReason.type === 'bgtask') {
return `Terminal command is running in the background in ${terminalDesc}. Here were the outputs after ${TERMINAL_BG_WAIT_TIME} seconds:\n${result_}`
}
else if (resolveReason.type === 'toofull') {
return `Terminal command executed in terminal ${terminalDesc}. Command was interrupted because output was too long. Result:\n${result_}`
}
else if (resolveReason.type === 'done') {
return `Terminal command executed in terminal ${terminalDesc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
// normal command
else {
if (resolveReason.type === 'timeout') {
return `Terminal command ran, but was interrupted after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. Full output:\n${result_}`
}
}
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
},
open_persistent_terminal: (_params, result) => {
const { terminalId } = result;
return `Successfully created background terminal with ID ${terminalId}`;
},
kill_persistent_terminal: (params, _result) => {
return `Successfully closed terminal ${params.terminalId}.`;
},
}
@@ -435,6 +535,23 @@ 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)
.slice(0, 100)
.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);
@@ -0,0 +1,55 @@
/*--------------------------------------------------------------------------------------
* 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 { mountVoidTooltip } from './react/out/void-tooltip/index.js';
import { h, getActiveWindow } from '../../../../base/browser/dom.js';
// Tooltip contribution that mounts the component at startup
export class TooltipContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.voidTooltip';
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.initializeTooltip();
}
private initializeTooltip(): 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) {
// Create a container element for the tooltip using h function
const tooltipContainer = h('div.void-tooltip-container').root;
workbench.appendChild(tooltipContainer);
// Mount the React component
this.instantiationService.invokeFunction((accessor: ServicesAccessor) => {
const result = mountVoidTooltip(tooltipContainer, accessor);
if (result && typeof result.dispose === 'function') {
this._register(toDisposable(result.dispose));
}
});
// Register cleanup for the DOM element
this._register(toDisposable(() => {
if (tooltipContainer.parentElement) {
tooltipContainer.parentElement.removeChild(tooltipContainer);
}
}));
}
}
}
// Register the contribution to be initialized during the AfterRestored phase
registerWorkbenchContribution2(TooltipContribution.ID, TooltipContribution, WorkbenchPhase.AfterRestored);
@@ -32,6 +32,7 @@ import './media/void.css'
// update (frontend part, also see platform/)
import './voidUpdateActions.js'
import './convertToLLMMessageWorkbenchContrib.js'
// tools
import './toolsService.js'
@@ -46,6 +47,15 @@ import './metricsPollService.js'
// helper services
import './helperServices/consistentItemService.js'
// register selection helper
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
@@ -11,7 +11,7 @@ import { Widget } from '../../../../base/browser/ui/widget.js';
import { IOverlayWidget, ICodeEditor, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js'
import { mountVoidCommandBar } from './react/out/void-editor-widgets-tsx/index.js'
import { deepClone } from '../../../../base/common/objects.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
@@ -173,7 +173,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
}
}))
this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => {
this._register(this._editCodeService.onDidChangeDiffsInDiffZoneNotStreaming(e => {
for (const uri of this._listenToTheseURIs) {
if (e.uri.fsPath !== uri.fsPath) continue
// --- sortedURIs: no change
@@ -362,8 +362,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationType.Delayed); // delayed is needed here :(
// registerWorkbenchContribution2(VoidCommandBarService.ID, VoidCommandBarService, WorkbenchPhase.BlockRestore);
export type VoidCommandBarProps = {
uri: URI | null;
@@ -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);
@@ -0,0 +1,282 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js';
import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';
import { ICursorSelectionChangedEvent } from '../../../../editor/common/cursorEvents.js';
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
import { Selection } from '../../../../editor/common/core/selection.js';
import { RunOnceScheduler } from '../../../../base/common/async.js';
import * as dom from '../../../../base/browser/dom.js';
import { mountVoidSelectionHelper } from './react/out/void-editor-widgets-tsx/index.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
import { getLengthOfTextPx } from './editCodeService.js';
const minDistanceFromRightPx = 400;
const minLeftPx = 60;
export type VoidSelectionHelperProps = {
rerenderKey: number // alternates between 0 and 1
}
export class SelectionHelperContribution extends Disposable implements IEditorContribution, IOverlayWidget {
public static readonly ID = 'editor.contrib.voidSelectionHelper';
// react
private _rootHTML: HTMLElement;
private _rerender: (props?: any) => void = () => { };
private _rerenderKey: number = 0;
private _reactComponentDisposable: IDisposable | null = null;
// internal
private _isVisible = false;
private _showScheduler: RunOnceScheduler;
private _lastSelection: Selection | null = null;
constructor(
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IVoidSettingsService private readonly _voidSettingsService: IVoidSettingsService
) {
super();
// Create the container element for React component
const { root, content } = dom.h('div@root', [
dom.h('div@content', [])
]);
// Set styles for container
root.style.position = 'absolute';
root.style.display = 'none'; // Start hidden
root.style.pointerEvents = 'none';
root.style.marginLeft = '16px';
// Initialize React component
this._instantiationService.invokeFunction(accessor => {
if (this._reactComponentDisposable) {
this._reactComponentDisposable.dispose();
}
const res = mountVoidSelectionHelper(content, accessor);
if (!res) return;
this._reactComponentDisposable = res;
this._rerender = res.rerender;
this._register(this._reactComponentDisposable);
});
this._rootHTML = root;
// Register as overlay widget
this._editor.addOverlayWidget(this);
// Use scheduler to debounce showing widget
this._showScheduler = new RunOnceScheduler(() => {
if (this._lastSelection) {
this._showHelperForSelection(this._lastSelection);
}
}, 50);
// Register event listeners
this._register(this._editor.onDidChangeCursorSelection(e => this._onSelectionChange(e)));
// Add a flag to track if mouse is over the widget
let isMouseOverWidget = false;
this._rootHTML.addEventListener('mouseenter', () => {
isMouseOverWidget = true;
});
this._rootHTML.addEventListener('mouseleave', () => {
isMouseOverWidget = false;
});
// Only hide helper when text editor loses focus and mouse is not over the widget
this._register(this._editor.onDidBlurEditorText(() => {
if (!isMouseOverWidget) {
this._hideHelper();
}
}));
this._register(this._editor.onDidScrollChange(() => this._updatePositionIfVisible()));
this._register(this._editor.onDidLayoutChange(() => this._updatePositionIfVisible()));
}
// IOverlayWidget implementation
public getId(): string {
return SelectionHelperContribution.ID;
}
public getDomNode(): HTMLElement {
return this._rootHTML;
}
public getPosition(): IOverlayWidgetPosition | null {
return null; // We position manually
}
private _onSelectionChange(e: ICursorSelectionChangedEvent): void {
if (!this._editor.hasModel()) {
return;
}
if (this._editor.getModel().uri.scheme !== 'file') {
return;
}
const selection = this._editor.getSelection();
if (!selection || selection.isEmpty()) {
this._hideHelper();
return;
}
// Get selection text to check if it's worth showing the helper
const text = this._editor.getModel()!.getValueInRange(selection);
if (text.length < 3) {
this._hideHelper();
return;
}
// Store selection
this._lastSelection = new Selection(
selection.startLineNumber,
selection.startColumn,
selection.endLineNumber,
selection.endColumn
);
this._showScheduler.schedule();
}
// Update the _showHelperForSelection method to work with the React component
private _showHelperForSelection(selection: Selection): void {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel()!;
// get the longest length of the nearest neighbors of the target
const { tabSize: numSpacesInTab } = model.getFormattingOptions();
const spaceWidth = this._editor.getOption(EditorOption.fontInfo).spaceWidth;
const tabWidth = numSpacesInTab * spaceWidth;
const numLinesModel = model.getLineCount()
// Calculate right edge of visible editor area
const editorWidthPx = this._editor.getLayoutInfo().width;
const maxLeftPx = editorWidthPx - minDistanceFromRightPx
// returns the position where the box should go on the targetLine
const getBoxPosition = (targetLine: number): { top: number, left: number } => {
const targetPosition = this._editor.getScrolledVisiblePosition({ lineNumber: targetLine, column: 1 }) ?? { left: 0, top: 0 };
const { top: targetTop, left: targetLeft } = targetPosition
let targetWidth = 0;
for (let i = targetLine; i <= targetLine + 1; i++) {
// if not in range, continue
if (!(i >= 1) || !(i <= numLinesModel)) continue;
const content = model.getLineContent(i);
const currWidth = getLengthOfTextPx({
tabWidth,
spaceWidth,
content
})
targetWidth = Math.max(targetWidth, currWidth);
}
return {
top: targetTop,
left: targetLeft + targetWidth,
};
}
// Calculate the middle line of the selection
const startLine = selection.startLineNumber;
const endLine = selection.endLineNumber;
// const middleLine = Math.floor(startLine + (endLine - startLine) / 2);
const targetLine = endLine - startLine + 1 <= 2 ? startLine : startLine + 2;
let boxPos = getBoxPosition(targetLine);
// if the position of the box is too far to the right, keep searching for a good position
const lineDeltasToTry = [-1, -2, -3, 1, 2, 3];
if (boxPos.left > maxLeftPx) {
for (const lineDelta of lineDeltasToTry) {
boxPos = getBoxPosition(targetLine + lineDelta);
if (boxPos.left <= maxLeftPx) {
break;
}
}
}
if (boxPos.left > maxLeftPx) { // if still not found, make it 2 lines before
boxPos = getBoxPosition(targetLine - 2)
}
// Position the helper element at the end of the middle line but ensure it's visible
const xPosition = Math.max(Math.min(boxPos.left, maxLeftPx), minLeftPx);
const yPosition = boxPos.top;
// Update the React component position
this._rootHTML.style.left = `${xPosition}px`;
this._rootHTML.style.top = `${yPosition}px`;
this._rootHTML.style.display = 'flex'; // Show the container
this._isVisible = true;
// rerender
const enabled = this._voidSettingsService.state.globalSettings.showInlineSuggestions
&& this._editor.hasTextFocus() // needed since VS Code counts unfocused selections as selections, which causes this to rerender when it shouldnt (bad ux)
if (enabled) {
this._rerender({ rerenderKey: this._rerenderKey } satisfies VoidSelectionHelperProps)
this._rerenderKey = (this._rerenderKey + 1) % 2;
// this._reactComponentRerender();
}
}
private _hideHelper(): void {
this._rootHTML.style.display = 'none';
this._isVisible = false;
this._lastSelection = null;
}
private _updatePositionIfVisible(): void {
if (!this._isVisible || !this._lastSelection || !this._editor.hasModel()) {
return;
}
this._showHelperForSelection(this._lastSelection);
}
override dispose(): void {
this._hideHelper();
if (this._reactComponentDisposable) {
this._reactComponentDisposable.dispose();
}
this._editor.removeOverlayWidget(this);
this._showScheduler.dispose();
super.dispose();
}
}
// Register the contribution
registerEditorContribution(SelectionHelperContribution.ID, SelectionHelperContribution, EditorContributionInstantiation.Eager);
@@ -8,57 +8,121 @@ 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,
label: `Keep current version`,
tooltip: '',
class: undefined,
run: () => {
notifController.close()
}
}]
},
})
const d = notifController.onDidClose(() => {
notifyYesUpdate(notifService, res)
d.dispose()
})
}
const notifyNoUpdate = (notifService: INotificationService) => {
notifService.notify({
}
}
else {
actions = undefined
}
const notifController = notifService.notify({
severity: Severity.Info,
message: 'Void is up-to-date!',
message: message,
sticky: true,
progress: actions ? { worked: 0, total: 100 } : undefined,
actions: actions,
})
// const d = notifController.onDidClose(() => {
// notifyYesUpdate(notifService, res)
// d.dispose()
// })
}
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)!`
@@ -70,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 {
@@ -84,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()
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)
}
})
@@ -97,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()
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
@@ -1,29 +1,50 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../base/common/uri.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { AnthropicReasoning } from './sendLLMMessageTypes.js';
import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { ToolName } from './prompt/prompts.js';
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
export type ToolMessage<T extends ToolName> = {
role: 'tool';
name: T; // internal use
paramsStr: string; // internal use
id: string; // apis require this tool use id
content: string; // give this result to LLM
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, }
// if rejected, don't show in chat
result:
| { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], }
| { type: 'error'; params: ToolCallParams[T] | undefined; value: string }
| { type: 'rejected'; params: ToolCallParams[T] }
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
| { type: '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] }
) // user rejected
export type DecorativeCanceledTool = {
role: 'interrupted_streaming_tool';
name: ToolName;
}
export type ToolRequestApproval<T extends ToolName> = {
role: 'tool_request';
name: T; // internal use
params: ToolCallParams[T]; // internal use
paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
id: string; // proposed tool's id
// checkpoints
export type CheckpointEntry = {
role: 'checkpoint';
type: 'user_edit' | 'tool_edit';
voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined };
userModifications: {
voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined };
};
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
@@ -37,44 +58,37 @@ export type ChatMessage =
}
} | {
role: 'assistant';
content: string; // content received from LLM - allowed to be '', will be replaced with (empty)
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
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
}
| ToolMessage<ToolName>
| ToolRequestApproval<ToolName>
| DecorativeCanceledTool
| CheckpointEntry
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
export type CodeSelection = {
type: 'Selection';
fileURI: URI;
language: string;
selectionStr: string;
range: IRange;
state: {
isOpened: boolean;
wasAddedAsCurrentFile: boolean;
};
}
export type FileSelection = {
// one of the square items that indicates a selection in a chat bubble
export type StagingSelectionItem = {
type: 'File';
fileURI: URI;
uri: URI;
language: string;
selectionStr: null;
range: null;
state: {
isOpened: boolean;
wasAddedAsCurrentFile: boolean;
};
state: { wasAddedAsCurrentFile: boolean; };
} | {
type: 'CodeSelection';
range: [number, number];
uri: URI;
language: string;
state: { wasAddedAsCurrentFile: boolean; };
} | {
type: 'Folder';
uri: URI;
language?: undefined;
state?: undefined;
}
export type StagingSelectionItem = CodeSelection | FileSelection
// a link to a symbol (an underlined link to a piece of code)
export type CodespanLocationLink = {
uri: URI, // we handle serialization for this
displayText: string,
@@ -0,0 +1,10 @@
import { URI } from '../../../../base/common/uri.js';
export type VoidDirectoryItem = {
uri: URI;
name: string;
isSymbolicLink: boolean;
children: VoidDirectoryItem[] | null;
isDirectory: boolean;
isGitIgnoredDirectory: false | { numChildren: number }; // if directory is gitignored, we ignore children
}
@@ -0,0 +1,119 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../base/common/uri.js';
export type ComputedDiff = {
type: 'edit';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
code: string;
startLine: number; // 1-indexed
endLine: number;
} | {
type: 'insertion';
// originalCode: string;
originalStartLine: number; // insertion starts on column 0 of this
// originalEndLine: number;
code: string;
startLine: number;
endLine: number;
} | {
type: 'deletion';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
// code: string;
startLine: number; // deletion starts on column 0 of this
// endLine: number;
}
// ---------- Diff types ----------
export type CommonZoneProps = {
diffareaid: number;
startLine: number;
endLine: number;
_URI: URI; // typically we get the URI from model
}
export type CtrlKZone = {
type: 'CtrlKZone';
originalCode?: undefined;
editorId: string; // the editor the input lives on
// _ means anything we don't include if we clone it
_mountInfo: null | {
textAreaRef: { current: HTMLTextAreaElement | null }
dispose: () => void;
refresh: () => void;
}
_linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
} & CommonZoneProps
export type TrackingZone<T> = {
type: 'TrackingZone';
metadata: T;
originalCode?: undefined;
editorId?: undefined;
_removeStylesFns?: undefined;
} & CommonZoneProps
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
export type Diff = {
diffid: number;
diffareaid: number; // the diff area this diff belongs to, "computed"
} & ComputedDiff
export type DiffZone = {
type: 'DiffZone',
originalCode: string;
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
_streamState: {
isStreaming: true;
streamRequestIdRef: { current: string | null };
line: number;
} | {
isStreaming: false;
streamRequestIdRef?: undefined;
line?: undefined;
};
editorId?: undefined;
linkedStreamingDiffZone?: undefined;
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
} & CommonZoneProps
export const diffAreaSnapshotKeys = [
'type',
'diffareaid',
'originalCode',
'startLine',
'endLine',
'editorId',
] as const satisfies (keyof DiffArea)[]
export type DiffAreaSnapshotEntry<DiffAreaType extends DiffArea = DiffArea> = Pick<DiffAreaType, typeof diffAreaSnapshotKeys[number]>
export type VoidFileSnapshot = {
snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshotEntry>;
entireFileCode: string;
}
@@ -3,10 +3,8 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { OnText } from '../sendLLMMessageTypes.js'
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
class SurroundingsRemover {
export class SurroundingsRemover {
readonly originalS: string
i: number
j: number
@@ -60,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)
@@ -154,6 +153,7 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
const foundMid = pm.removePrefix(`<${midTag}>`)
if (foundMid) {
pm.removeSuffix(`\n`) // sometimes outputs \n
pm.removeSuffix(`</${midTag}>`)
}
const s = pm.value()
@@ -174,7 +174,7 @@ export type ExtractedSearchReplaceBlock = {
// JS substring swaps indices, so "ab".substr(1,0) will NOT be '', it will be 'a'!
const voidSubstr = (str: string, start: number, end: number) => end < start ? '' : str.substring(start, end)
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
export const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
// for each prefix
for (let i = anyPrefix.length; i >= 1; i--) { // i >= 1 because must not be empty string
const prefix = anyPrefix.slice(0, i)
@@ -250,122 +250,6 @@ export const extractSearchReplaceBlocks = (str: string) => {
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => {
let latestAddIdx = 0 // exclusive index in fullText_
let foundTag1 = false
let foundTag2 = false
let fullTextSoFar = ''
let fullReasoningSoFar = ''
let onText_ = onText
onText = (params) => {
onText_(params)
}
const newOnText: OnText = ({ fullText: fullText_, ...p }) => {
// until found the first think tag, keep adding to fullText
if (!foundTag1) {
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
if (endsWithTag1) {
// console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ })
// wait until we get the full tag or know more
return
}
// if found the first tag
const tag1Index = fullText_.indexOf(thinkTags[0])
if (tag1Index !== -1) {
// console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ })
foundTag1 = true
// Add text before the tag to fullTextSoFar
fullTextSoFar += fullText_.substring(0, tag1Index)
// Update latestAddIdx to after the first tag
latestAddIdx = tag1Index + thinkTags[0].length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar })
// add the text to fullText
fullTextSoFar = fullText_
latestAddIdx = fullText_.length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// at this point, we found <tag1>
// until found the second think tag, keep adding to fullReasoning
if (!foundTag2) {
const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1])
if (endsWithTag2) {
// console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar })
// wait until we get the full tag or know more
return
}
// if found the second tag
const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx)
if (tag2Index !== -1) {
// console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar })
foundTag2 = true
// Add everything between first and second tag to reasoning
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
// Update latestAddIdx to after the second tag
latestAddIdx = tag2Index + thinkTags[1].length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// add the text to fullReasoning (content after first tag but before second tag)
// console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar })
// If we have more text than we've processed, add it to reasoning
if (fullText_.length > latestAddIdx) {
fullReasoningSoFar += fullText_.substring(latestAddIdx)
latestAddIdx = fullText_.length
}
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// at this point, we found <tag2> - content after the second tag is normal text
// console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar })
// Add any new text after the closing tag to fullTextSoFar
if (fullText_.length > latestAddIdx) {
fullTextSoFar += fullText_.substring(latestAddIdx)
latestAddIdx = fullText_.length
}
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
}
return newOnText
}
export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => {
const tag1Idx = fullText_.indexOf(thinkTags[0])
const tag2Idx = fullText_.indexOf(thinkTags[1])
if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning
if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning
const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx)
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
return { fullText, fullReasoning }
}
File diff suppressed because it is too large Load Diff
@@ -3,19 +3,37 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../../base/common/uri.js';
import { os } from '../helpers/systemInfo.js';
import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { InternalToolInfo } from '../toolsServiceTypes.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
import { approvalTypeOfToolName, ToolResultType } 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
// tool info
export const MAX_FILE_CHARS_PAGE = 500_000
export const MAX_CHILDREN_URIs_PAGE = 500
// terminal tool info
export const MAX_TERMINAL_CHARS = 100_000
export const MAX_TERMINAL_INACTIVE_TIME = 8 // seconds
// Maximum character limits for prefix and suffix context
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
// ======================================================== tools ========================================================
const changesExampleContent = `\
// ... existing code ...
// {{change 1}}
@@ -25,273 +43,426 @@ const changesExampleContent = `\
// {{change 3}}
// ... existing code ...`
const editToolDescription = `\
const editToolDescriptionExample = `\
${tripleTick[0]}
${changesExampleContent}
${tripleTick[1]}`
const fileNameEdit = `${tripleTick[0]}typescript
const fileNameEditExample = `${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
${changesExampleContent}
${tripleTick[1]}`
export type InternalToolInfo = {
name: string,
description: string,
params: {
[paramName: string]: { description: string }
},
}
// ======================================================== tools ========================================================
const paginationHelper = {
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
const uriParam = (object: string) => ({
uri: { description: `The FULL path to the ${object}.` }
})
const paginationParam = {
page_number: { description: 'Optional. The page number of the result. Default is 1.' }
} as const
// export type SnakeCase<S extends string> =
// // exact acronym URI
// S extends 'URI' ? 'uri'
// // suffix URI: e.g. 'rootURI' -> snakeCase('root') + '_uri'
// : S extends `${infer Prefix}URI` ? `${SnakeCase<Prefix>}_uri`
// // default: for each char, prefix '_' on uppercase letters
// : S extends `${infer C}${infer Rest}`
// ? `${C extends Lowercase<C> ? C : `_${Lowercase<C>}`}${SnakeCase<Rest>}`
// : S;
// export type SnakeCaseKeys<T extends Record<string, any>> = {
// [K in keyof T as SnakeCase<Extract<K, string>>]: T[K]
// };
export const voidTools = {
// export const voidTools
// : {
// [T in keyof ToolCallParams]: {
// name: string;
// description: string;
// params: {
// [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string }
// }
// }
// }
// = {
// --- context-gathering (read/search/list) ---
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
description: `Returns full contents of a given file.`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param,
...uriParam('file'),
start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' },
end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' },
...paginationParam,
},
},
list_dir: {
name: 'list_dir',
description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`,
ls_dir: {
name: 'ls_dir',
description: `Lists all files and folders in the given URI.`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param,
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
...paginationParam,
},
},
pathname_search: {
name: 'pathname_search',
description: `Returns all pathnames that match a given \`find\`-style query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
get_dir_tree: {
name: 'get_dir_tree',
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
...uriParam('folder')
}
},
// pathname_search: {
// name: 'pathname_search',
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
search_pathnames_only: {
name: 'search_pathnames_only',
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
params: {
query: { description: `Your query for the search.` },
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
...paginationParam,
},
},
grep_search: {
name: 'grep_search',
description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`,
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: { type: 'string', description: undefined },
...paginationHelper.param,
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
...paginationParam,
},
},
// add new search_in_file tool
search_in_file: {
name: 'search_in_file',
description: `Returns an array of all the start line numbers where the content appears in the file.`,
params: {
...uriParam('file'),
query: { description: 'The string or regex to search for in the file.' },
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
}
},
read_lint_errors: {
name: 'read_lint_errors',
description: `Returns all lint errors on a given file.`,
params: {
...uriParam('file'),
},
},
// --- editing (create/delete) ---
create_uri: {
name: 'create_uri',
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
create_file_or_folder: {
name: 'create_file_or_folder',
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
params: {
uri: { type: 'string', description: undefined },
...uriParam('file or folder'),
},
},
delete_uri: {
name: 'delete_uri',
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
delete_file_or_folder: {
name: 'delete_file_or_folder',
description: `Delete a file or folder at the given path.`,
params: {
uri: { type: 'string', description: undefined },
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
...uriParam('file or folder'),
params: { description: 'Optional. Return -r here to delete recursively.' }
},
},
edit: { // APPLY TOOL
name: 'edit',
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
edit_file: { // APPLY TOOL
name: 'edit_file',
description: `Edits the contents of a file given the file's URI and a description.`,
params: {
uri: { type: 'string', description: undefined },
changeDescription: {
type: 'string', description: `\
- Your changeDescription should be 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, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
- You must output your description in triple backticks.
Here's an example of a good description:\n${editToolDescription}.`
...uriParam('file'),
change_diff: {
description: `\
A code diff describing the change to make to the file. \
Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \
Your DIFF MUST be wrapped in triple backticks. \
NEVER re-write the whole file. Always bias towards writing as little as possible. \
Use comments like "// ... existing code ..." to condense your writing. \
Here's an example of a good output:\n${editToolDescriptionExample}`
}
},
},
terminal_command: {
name: 'terminal_command',
description: `Executes a terminal command.`,
run_command: {
name: 'run_command',
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`,
params: {
command: { type: 'string', description: 'The terminal command to execute.' },
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
command: { description: 'The terminal command to run.' },
bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' },
},
},
open_persistent_terminal: {
name: 'open_persistent_terminal',
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
params: {}
},
kill_persistent_terminal: {
name: 'kill_persistent_terminal',
description: `Closes a BG terminal with the given ID.`,
params: { terminal_id: { description: `The terminal ID to interrupt and close.` } }
}
// go_to_definition
// go_to_usages
} satisfies { [name: string]: InternalToolInfo }
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
export type ToolName = keyof ToolResultType
export const toolNames = Object.keys(voidTools) as ToolName[]
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
const toolNamesSet = new Set<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
export const availableTools = (chatMode: ChatMode) => {
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
: undefined
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
return tools
}
const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
return `${tools.map((t, i) => {
const params = Object.keys(t.params).map(paramName => `<${paramName}>${t.params[paramName].description}</${paramName}>`).join('\n')
return `\
${i + 1}. ${t.name}
Description: ${t.description}
Format:
<${t.name}>${!params ? '' : `\n${params}`}
</${t.name}>`
}).join('\n\n')}`
}
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
return `\
<${toolName}>${!params ? '' : `\n${params}`}
</${toolName}>`
.replace('\t', ' ')
}
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
const tools = availableTools(chatMode)
if (!tools || tools.length === 0) return null
const toolXMLDefinitions = (`\
Available tools:
${toolCallDefinitionsXMLString(tools)}`)
const toolCallXMLGuidelines = (`\
Tool calling details:
- To call a tool, write its name and parameters in one of the XML formats specified above.
- After you write the tool call, you must STOP and WAIT for the result.
- All parameters are REQUIRED unless noted otherwise.
- 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.`)
return `\
${toolXMLDefinitions}
${toolCallXMLGuidelines}`
}
// ======================================================== chat (normal, gather, agent) ========================================================
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.`
: mode === 'normal' ? `to assist the user with their coding tasks.`
: ''}
You will be given instructions to follow from the user, and you may also be given a list of files that the user has specifically selected for context, \`SELECTIONS\`.
Please assist the user with their query.`)
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \
${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.`
: mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.`
: mode === 'normal' ? `to assist the user with their coding tasks.`
: ''}
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
Please assist the user with their query. The user's query is never invalid.
${/* system info */''}
The user's system information is as follows:
const sysInfo = (`Here is the user's system information:
<system_info>
- ${os}
- Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'}
${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\
- Existing terminal IDs: ${runningTerminalIds.join(', ')}
`: '\n'}
${/* tool use */ mode === 'agent' || mode === 'gather' ? `\
You will be given tools you can call.
${mode === 'agent' ? `\
- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.
- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.
- You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.
- ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`
: mode === 'gather' ? `\
- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.
- You should extensively read files, types, etc and gather relevant context.`
: ''}
- If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc.
- NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results.
- Some tools only work if the user has a workspace open.${mode === 'agent' ? `
- NEVER modify a file outside the user's workspace(s) without permission from the user.` : ''}
\
`: `\
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
\
`}
${/* code blocks */ mode === 'agent' ? `\
Behavior:
- Always use tools (edit, terminal, etc) to take actions and implement changes. Don't just describe them.
- Prioritize taking as many steps as you need to complete your request over stopping early.\
`: `\
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
- The first line of the code block must be the FULL PATH of the file you want to change.
- The remaining contents should be 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, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
Here's an example of a good code block:\n${fileNameEdit}.\
`}
${/* misc */''}
Misc:
- Do not make things up.
- Do not be lazy.
- NEVER re-write the entire file.
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\
`
// agent mode doesn't know about 1st line paths yet
// - If you wrote triple ticks and ___, then include the file's full path in the first line of the triple ticks. This is only for display purposes to the user, and it's preferred but optional. Never do this in a tool parameter, or if there's ambiguity about the full path.
- Open workspaces:
${workspaceFolders.join('\n') || 'NO WORKSPACE OPEN'}
- Active file:
${activeURI}
- Open files:
${openedURIs.join('\n') || 'NO OPENED EDITORS'}${''/* separator */}${mode === 'agent' && runningTerminalIds.length !== 0 ? `
- Existing terminal IDs: ${runningTerminalIds.join(', ')}` : ''}
</system_info>`)
type FileSelnLocal = { fileURI: URI, language: string, content: string }
const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => {
return `\
${fileURI.fsPath}
${tripleTick[0]}${language}
${content}
${tripleTick[1]}
`
}
const stringifyCodeSelection = ({ fileURI, language, selectionStr, range }: CodeSelection) => {
return `\
${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber})
${tripleTick[0]}${language}
${selectionStr}
${tripleTick[1]}
`
}
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => {
if (fileSelections.length === 0) return null
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
const { model } = await voidModelService.getModelSafe(sel.fileURI)
const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr
return { ...sel, content }
}))
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
}
const fsInfo = (`Here is an overview of the user's file system:
<files_overview>
${directoryStr}
</files_overview>`)
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
return codeSelections.map(sel => {
stringifyCodeSelection(sel)
}).join('\n') || null
}
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => {
if (!currSelns) return ''
return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n')
}
const details: string[] = []
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => {
const selnsStr = stringifySelectionNames(currSelns)
let str = ''
if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` }
str += `\nINSTRUCTIONS\n${instructions}`
return str;
};
export const chat_selectionsString = async (
prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null,
voidModelService: IVoidModelService,
) => {
// ADD IN FILES AT TOP
const allSelections = [...currSelns || [], ...prevSelns || []]
if (allSelections.length === 0) return null
const codeSelections: CodeSelection[] = []
const fileSelections: FileSelection[] = []
const filesURIs = new Set<string>()
for (const selection of allSelections) {
if (selection.type === 'Selection') {
codeSelections.push(selection)
}
else if (selection.type === 'File') {
const fileSelection = selection
const path = fileSelection.fileURI.fsPath
if (!filesURIs.has(path)) {
filesURIs.add(path)
fileSelections.push(fileSelection)
}
}
if (mode === 'agent' || mode === 'gather') {
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
details.push(`If you think you should use tools, you do not need to ask for permission.`)
details.push('Only use ONE tool call at a time.')
details.push(`NEVER say something like "I'm going to use \`tool_name\`". Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc.`)
details.push(`Many tools only work if the user has a workspace open.`)
}
else {
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
}
const filesStr = await stringifyFileSelections(fileSelections, voidModelService)
const selnsStr = stringifyCodeSelections(codeSelections)
if (mode === 'agent') {
details.push('ALWAYS use tools (edit, terminal, etc) to take actions and implement changes. For example, if you would like to edit a file, you MUST use a tool.')
details.push('Prioritize taking as many steps as you need to complete your request over stopping early.')
details.push(`You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.`)
details.push(`ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`)
details.push(`NEVER modify a file outside the user's workspace(s) without permission from the user.`)
}
if (mode === 'gather') {
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.`)
}
if (mode === 'gather' || mode === 'normal') {
details.push(`If you write any code blocks, please use this format:
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents of the file should proceed as usual.`)
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents should be \
a 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. \
Here's an example of a good edit suggestion:
${fileNameEditExample}.`)
}
details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`)
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
details.push(`Today's date is ${new Date().toDateString()}.`)
const importantDetails = (`Important notes:
${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
// return answer
const ansStrs: string[] = []
ansStrs.push(header)
ansStrs.push(sysInfo)
if (toolDefinitions) ansStrs.push(toolDefinitions)
ansStrs.push(importantDetails)
ansStrs.push(fsInfo)
const fullSystemMsgStr = ansStrs
.join('\n\n\n')
.trim()
.replace('\t', ' ')
return fullSystemMsgStr
const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n')
return fileContents || null
}
export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => {
if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
else return userMessage
// // log all prompts
// for (const chatMode of ['agent', 'gather', 'normal'] satisfies ChatMode[]) {
// console.log(`========================================= SYSTEM MESSAGE FOR ${chatMode} ===================================\n`,
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', runningTerminalIds: [], directoryStr: 'lol', }))
// }
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
) => {
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
let selnsStrs: string[] = []
if (opts.type === 'references') {
selnsStrs = currSelns?.map((s) => {
if (s.type === 'File') return `${s.uri.fsPath}`
if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}`
if (s.type === 'Folder') return `${s.uri.fsPath}/`
return ''
}) ?? []
}
if (opts.type === 'fullCode') {
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
if (s.type === 'File' || s.type === 'CodeSelection') {
const voidModelService = opts.voidModelService
const { model } = await voidModelService.getModelSafe(s.uri)
if (!model) return ''
const val = model.getValue(EndOfLinePreference.LF)
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
return str
}
if (s.type === 'Folder') {
// TODO
return ''
}
return ''
}) ?? [])
}
const selnsStr = selnsStrs.join('\n') ?? ''
let str = ''
str += `${instructions}`
if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}`
return str;
}
@@ -337,9 +508,10 @@ export const DIVIDER = `=======`
export const FINAL = `>>>>>>> UPDATED`
export const searchReplace_systemMessage = `\
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff.
The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`.
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
Format your SEARCH/REPLACE blocks as follows:
${tripleTick[0]}
${ORIGINAL}
// ... original code goes here
@@ -348,22 +520,28 @@ ${DIVIDER}
${FINAL}
${tripleTick[1]}
You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make.
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out.
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.
4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks.
2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change.
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.
3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output.
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.
4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code.
6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible.
7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.
## EXAMPLE 1
DIFF
${tripleTick[0]}
// ... existing code
let x = 6.5
// ... existing code
${tripleTick[1]}
ORIGINAL_FILE
${tripleTick[0]}
let w = 5
@@ -372,15 +550,6 @@ let y = 7
let z = 8
${tripleTick[1]}
CHANGE
Make x equal to 6.5, not 6.
${tripleTick[0]}
// ... existing code
let x = 6.5
// ... existing code
${tripleTick[1]}
## ACCEPTED OUTPUT
${tripleTick[0]}
${ORIGINAL}
@@ -392,11 +561,14 @@ ${tripleTick[1]}
`
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
ORIGINAL_FILE
${originalCode}
DIFF
${applyStr}
CHANGE
${applyStr}`
ORIGINAL_FILE
${tripleTick[0]}
${originalCode}
${tripleTick[1]}
`
@@ -406,8 +578,6 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
const fullFileLines = fullFileStr.split('\n')
// we can optimize this later
const MAX_PREFIX_SUFFIX_CHARS = 20_000
/*
a
@@ -480,10 +650,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
@@ -513,6 +689,7 @@ ${tripleTick[1]}).`
/*
// ======================================================== ai search/replace ========================================================
@@ -8,7 +8,7 @@ import { ILLMMessageService } from './sendLLMMessageService.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, VLLMModelResponse } from './sendLLMMessageTypes.js';
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './sendLLMMessageTypes.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@@ -46,6 +46,7 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_didFillInProviderSettings', 'endpoint'],
vLLM: ['_didFillInProviderSettings', 'endpoint'],
lmStudio: ['_didFillInProviderSettings', 'endpoint'],
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
@@ -142,6 +143,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
state: RefreshModelStateOfProvider = {
ollama: { state: 'init', timeoutId: null },
vLLM: { state: 'init', timeoutId: null },
lmStudio: { state: 'init', timeoutId: null },
}
@@ -160,18 +162,18 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
}
}
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
: providerName === 'vLLM' ? this.llmMessageService.vLLMList
: () => { }
: this.llmMessageService.openAICompatibleList
listFn({
providerName,
onSuccess: ({ models }) => {
// set the models to the detected models
this.voidSettingsService.setAutodetectedModels(
providerName,
models.map(model => {
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
else if (providerName === 'vLLM') return (model as VLLMModelResponse).id;
else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id;
else if (providerName === 'lmStudio') return (model as OpenaiCompatibleModelResponse).id;
else throw new Error('refreshMode fn: unknown provider', providerName);
}),
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './sendLLMMessageTypes.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './sendLLMMessageTypes.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
@@ -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');
@@ -23,7 +22,7 @@ export interface ILLMMessageService {
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
abort: (requestId: string) => void;
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
vLLMList: (params: ServiceModelListParams<VLLMModelResponse>) => void;
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
}
@@ -47,12 +46,12 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) },
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) },
},
vLLM: {
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<VLLMModelResponse>) => void) },
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<VLLMModelResponse>) => void) },
openAICompat: {
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) },
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) },
}
} satisfies {
[providerName: string]: {
[providerName in 'ollama' | 'openAICompat']: {
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<any>) => void) },
error: { [eventId: string]: ((params: EventModelListOnErrorParams<any>) => void) },
}
@@ -71,14 +70,31 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
// llm
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) }))
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._clearChannelHooks(e.requestId) }))
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._clearChannelHooks(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
// ollama .list()
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.success[e.requestId]?.(e) }))
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.error[e.requestId]?.(e) }))
this._register((this.channel.listen('onSuccess_list_vLLM') satisfies Event<EventModelListOnSuccessParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.success[e.requestId]?.(e) }))
this._register((this.channel.listen('onError_list_vLLM') satisfies Event<EventModelListOnErrorParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.error[e.requestId]?.(e) }))
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => {
this.llmMessageHooks.onText[e.requestId]?.(e)
}))
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
this.llmMessageHooks.onFinalMessage[e.requestId]?.(e);
this._clearChannelHooks(e.requestId)
}))
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
this.llmMessageHooks.onError[e.requestId]?.(e);
this._clearChannelHooks(e.requestId);
console.error('Error in LLMMessageService:', JSON.stringify(e))
}))
// .list()
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
this.listHooks.ollama.success[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
this.listHooks.ollama.error[e.requestId]?.(e)
}))
this._register((this.channel.listen('onSuccess_list_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
this.listHooks.openAICompat.success[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_list_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
this.listHooks.openAICompat.error[e.requestId]?.(e)
}))
}
@@ -98,6 +114,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 +123,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,
@@ -147,25 +160,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
}
vLLMList = (params: ServiceModelListParams<VLLMModelResponse>) => {
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
// add state for request id
const requestId_ = generateUuid();
this.listHooks.vLLM.success[requestId_] = onSuccess
this.listHooks.vLLM.error[requestId_] = onError
this.listHooks.openAICompat.success[requestId_] = onSuccess
this.listHooks.openAICompat.error[requestId_] = onError
this.channel.call('vLLMList', {
this.channel.call('openAICompatibleList', {
...proxyParams,
settingsOfProvider,
providerName: 'vLLM',
requestId: requestId_,
} satisfies MainModelListParams<VLLMModelResponse>)
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
}
_clearChannelHooks(requestId: string) {
private _clearChannelHooks(requestId: string) {
delete this.llmMessageHooks.onText[requestId]
delete this.llmMessageHooks.onFinalMessage[requestId]
delete this.llmMessageHooks.onError[requestId]
@@ -173,8 +185,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
delete this.listHooks.ollama.success[requestId]
delete this.listHooks.ollama.error[requestId]
delete this.listHooks.vLLM.success[requestId]
delete this.listHooks.vLLM.error[requestId]
delete this.listHooks.openAICompat.success[requestId]
delete this.listHooks.openAICompat.error[requestId]
}
}
@@ -3,8 +3,8 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ToolName, InternalToolInfo } from './toolsServiceTypes.js'
import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { ToolName, ToolParamName } from './prompt/prompts.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@@ -27,38 +27,33 @@ 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; // result
name: string;
params: string;
id: string;
role: 'tool',
content: string;
tool_call_id: string;
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
export type ToolCallType = {
name: ToolName;
paramsStr: string;
id: string;
}
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
export type OnText = (p: { fullText: string; fullReasoning: string; fullToolName: string; fullToolParams: string; }) => void
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
export type OnError = (p: { message: string; fullError: Error | null }) => void
export type OnAbort = () => void
export type AbortRef = { current: (() => void) | null }
export type LLMFIMMessage = {
@@ -67,17 +62,39 @@ export type LLMFIMMessage = {
stopTokens: string[];
}
export type RawToolParamsObj = {
[paramName in ToolParamName]?: string;
}
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
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
export type OnError = (p: { message: string; fullError: Error | null }) => void
export type OnAbort = () => void
export type AbortRef = { current: (() => void) | null }
// service types
type SendLLMType = {
messagesType: 'chatMessages';
messages: LLMChatMessage[];
tools?: InternalToolInfo[];
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;
tools?: undefined;
separateSystemMessage?: undefined;
chatMode?: undefined;
}
// service types
export type ServiceSendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
@@ -96,8 +113,6 @@ export type SendLLMMessageParams = {
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
abortRef: AbortRef;
aiInstructions: string;
modelSelection: ModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined;
@@ -147,15 +162,13 @@ export type OllamaModelResponse = {
size_vram: number;
}
type OpenaiCompatibleModelResponse = {
export type OpenaiCompatibleModelResponse = {
id: string;
created: number;
object: 'model';
owned_by: string;
}
export type VLLMModelResponse = OpenaiCompatibleModelResponse
// params to the true list fn
@@ -168,12 +181,13 @@ export type ModelListParams<ModelResponse> = {
// params to the service
export type ServiceModelListParams<modelResponse> = {
providerName: RefreshableProviderName;
onSuccess: (param: { models: modelResponse[] }) => void;
onError: (param: { error: any }) => void;
}
type BlockedMainModelListParams = 'onSuccess' | 'onError'
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { providerName: RefreshableProviderName, requestId: string }
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }

Some files were not shown because too many files have changed in this diff Show More