Compare commits

...

276 Commits

Author SHA1 Message Date
Mathew Pareles 3e7e54b88f Merge branch 'model-selection' into HEAD 2025-02-25 18:48:38 -08:00
Mathew Pareles 1b43799371 prepare merg 2025-02-25 18:48:17 -08:00
Mathew Pareles 38370c751e + 2025-02-24 05:59:56 -08:00
Mathew Pareles 764b1a2ccf improve input box 2025-02-24 05:47:58 -08:00
Mathew Pareles 1dffbfb061 style 2025-02-24 04:09:09 -08:00
Mathew Pareles df6de2d7fc styles for tool use 2025-02-24 03:51:59 -08:00
Mathew Pareles 4ebfa91a83 fix URI staleness bug in local storage 2025-02-24 00:46:03 -08:00
Andrew Pareles 9f20476eea provider support progress
Co-authored-by: Mathew Pareles <mathewpareles@users.noreply.github.com>
2025-02-23 21:37:34 -08:00
Andrew Pareles fd5e523434 models 2025-02-23 16:58:43 -08:00
Andrew Pareles 2c2714273e partway through adding better support for more providers 2025-02-23 01:28:51 -08:00
Mathew Pareles c650091418 style 2025-02-22 21:11:58 -08:00
Mathew Pareles d96a9d5f6b fix typeerror 2025-02-22 21:11:58 -08:00
Mathew Pareles a9b3cc146b refactor 2025-02-22 21:11:58 -08:00
Mathew Pareles bdb897d032 tool UI draft 2025-02-22 21:11:58 -08:00
Andrew Pareles 2a876d8efe + 2025-02-22 21:08:34 -08:00
Andrew Pareles bf4392a52f safe 2025-02-22 17:53:14 -08:00
Andrew Pareles dd24c3180d + 2025-02-22 17:52:58 -08:00
Andrew Pareles f40acd76f1 endpoint 2025-02-22 17:52:28 -08:00
Andrew Pareles 96de4f028f remove unnecessary check 2025-02-22 04:35:56 -08:00
Andrew Pareles 2ecce822f3 fix react - tedious 2025-02-22 04:30:17 -08:00
Andrew Pareles ea10765abd remove applybox stuff from editCode 2025-02-22 03:41:16 -08:00
Andrew Pareles 16f6181395 + 2025-02-22 03:23:44 -08:00
Andrew Pareles 19ebf4a1a2 seems to work! some annoying react state stuff.. 2025-02-22 03:03:05 -08:00
Andrew Pareles 63b71dec24 should work, just need to debug 2025-02-22 00:40:46 -08:00
Andrew Pareles 1079893527 add codeBoxId, but now will change to uri 2025-02-21 23:31:06 -08:00
Andrew Pareles 64ac6d4a12 ctrlK 2025-02-21 21:09:03 -08:00
Andrew Pareles 9005de65a6 quick edit stream state? 2025-02-21 20:53:09 -08:00
Andrew Pareles 86dfc5521d more progress (still broken) + fix weird _useThread() + rm initStreamingDiffZoneId 2025-02-21 20:10:02 -08:00
Andrew Pareles b005b1e95a progress (BROKEN) 2025-02-21 18:53:55 -08:00
Andrew Pareles ce14986d2f split apply buttons 2025-02-21 16:19:01 -08:00
Andrew Pareles 06ce9e1017 settings styles + password field 2025-02-21 15:06:42 -08:00
Andrew Pareles 09f5d3e14b nitpick style 2025-02-21 00:19:58 -08:00
Andrew Pareles 73c3a8133a massively simplified, seems to work! 2025-02-20 23:59:16 -08:00
Andrew Pareles 8918a14404 almost there 2025-02-20 23:23:58 -08:00
Andrew Pareles 1959c53d75 progress 2025-02-20 23:23:55 -08:00
Andrew Pareles 33d80bed80 progress 2025-02-20 17:25:53 -08:00
Andrew Pareles 970f0bdb9d revert "Void's" on the button 2025-02-20 01:28:46 -08:00
Andrew Pareles 7711c74ef0 reorder 2025-02-20 01:21:56 -08:00
Andrew Pareles e14aab632d multiple find/replace blocks almost work. only 1 diffarea per find/replace application 2025-02-20 01:21:28 -08:00
Andrew Pareles 62c2622ced Void's settings 2025-02-19 17:57:40 -08:00
Andrew Pareles 0f26ee2288 misc fixes 2025-02-19 00:47:03 -08:00
Andrew Pareles 6a44d668db + 2025-02-19 00:16:25 -08:00
Mathew Pareles 858b6f6a91 file service read 2025-02-19 00:12:51 -08:00
Andrew Pareles 02f64b7ff6 finish tool pagination 2025-02-18 22:28:04 -08:00
Andrew Pareles ac1788ae9a tool pages work, improve prompt 2025-02-18 21:25:23 -08:00
Andrew Pareles 5699cf19f4 merge updates 2025-02-18 16:12:16 -08:00
Andrew Pareles 9ab9196b38 Merge remote-tracking branch 'origin/edit-chats' into model-selection 2025-02-18 16:12:11 -08:00
Mathew Pareles baa89cc17d dummy marker service 2025-02-18 14:14:38 -08:00
Andrew Pareles 667769c987 tool update; multiple SEARCH/REPLACE blocks 2025-02-17 23:57:59 -08:00
Mathew Pareles d3aa0bc3cc fix readfile 2025-02-17 17:56:20 -08:00
Andrew Pareles d3547134e7 get ready to add searchReplaceService 2025-02-17 15:22:24 -08:00
Mathew Pareles b92420012c minor 2025-02-17 15:18:11 -08:00
Andrew Pareles 0fd10f404e tool results in ChatMessage 2025-02-17 14:12:52 -08:00
Andrew Pareles 137e0068a3 add vllm 2025-02-17 01:50:28 -08:00
Andrew Pareles 366dbf0b52 add xAI and update system/tool information and system prompt 2025-02-17 01:06:21 -08:00
Mathew Pareles 89d08071fc scrollbar fix 2025-02-17 00:23:48 -08:00
Andrew Pareles 8a8ed1ac56 + 2025-02-17 00:20:01 -08:00
Andrew Pareles 491312218f + 2025-02-17 00:19:39 -08:00
Andrew Pareles b74b031906 + 2025-02-17 00:18:55 -08:00
Andrew Pareles 5608aca168 process 2025-02-17 00:17:14 -08:00
Andrew Pareles fecfbd924a error 2025-02-17 00:07:33 -08:00
Andrew Pareles a78a6169f8 tools + misc fixes 2025-02-17 00:07:11 -08:00
Andrew Pareles 628adedaec toolcalls 2025-02-16 20:51:42 -08:00
Andrew Pareles 432a1766af anthropic tool use fix 2025-02-16 20:50:07 -08:00
Mathew Pareles 74f8303803 discard star changes 2025-02-16 20:11:13 -08:00
Andrew Pareles 2bc3d67e39 minor tool use fix 2025-02-16 19:51:58 -08:00
Mathew Pareles 41fe5c50e2 fix scrollbars 2025-02-16 19:46:41 -08:00
Mathew Pareles 36b1b56690 bug 2025-02-16 19:46:41 -08:00
Mathew Pareles 249eee341d better context and file reading 2025-02-16 19:46:40 -08:00
Andrew Pareles af41d6a439 tool improvements 2025-02-16 03:23:37 -08:00
Andrew Pareles 7244d433dd tools should work! 2025-02-16 02:53:56 -08:00
Andrew Pareles 131493b5e1 tool structure 2025-02-16 00:43:46 -08:00
Andrew Pareles 8591d06244 tool use plugboard progress 2025-02-15 19:23:15 -08:00
Andrew Pareles 152e605856 tool progress! 2025-02-15 02:05:37 -08:00
Andrew Pareles bc6150aeac tools 2025-02-15 00:02:17 -08:00
Andrew Pareles 05d8b3a982 handle updates with version number instead of weird check 2025-02-15 00:01:37 -08:00
Andrew Pareles d91ec9da2e inlineDiffsService -> editCodeService 2025-02-14 21:43:38 -08:00
Mathew Pareles 198a948f6c star button commit 2025-02-14 21:30:20 -08:00
Andrew Pareles 6ad48ffa20 recognizedModels 2025-02-14 01:59:17 -08:00
Andrew Pareles 0975f1bf5f add developer info for models 2025-02-14 01:52:17 -08:00
Andrew Pareles 9cfcf396c1 update types for tool support 2025-02-14 01:05:10 -08:00
Andrew Pareles 0bcd88dad6 fix 2025-02-13 23:54:22 -08:00
Andrew Pareles 343ee5eb94 Apply name 2025-02-13 23:00:01 -08:00
Mathew Pareles d9e4679b65 breaking changes - ai regex 2025-02-13 22:58:42 -08:00
Andrew Pareles b01684393a streaming works! 2025-02-13 00:37:43 -08:00
Andrew Pareles 34b8027fb9 streaming progress 2025-02-13 00:08:23 -08:00
Andrew Pareles f7af9c336b tokenIdx 2025-02-12 21:58:57 -08:00
Andrew Pareles a479a32ad5 small fix 2025-02-12 21:55:58 -08:00
Andrew Pareles 7cdb003c47 stream state 2025-02-12 21:51:58 -08:00
Andrew Pareles 4f9f16b93f almost works (but doesn't delete old text) 2025-02-12 21:27:39 -08:00
Mathew Pareles 7dcb08b09a comment out unwanted code 2025-02-12 20:50:04 -08:00
Andrew Pareles 0dfa81f637 Search and replace? 2025-02-12 01:43:50 -08:00
Andrew Pareles d504daffa5 searchRepalce 2025-02-12 00:34:24 -08:00
Andrew Pareles fc97949887 autocomplete 2025-02-11 22:02:46 -08:00
Andrew Pareles 0346e90ca7 add parsing of <<< ORIGINAL, ===, FINAL >>> 2025-02-11 21:38:56 -08:00
Mathew Pareles efeb85b48b Merge remote-tracking branch 'origin/model-selection' into edit-chats 2025-02-11 18:44:20 -08:00
Mathew Pareles d698e6f3c1 prepare fast apply 2025-02-11 18:42:40 -08:00
Andrew Pareles 2161d740df minor change 2025-02-11 16:44:40 -08:00
Andrew Pareles 96b5b72d30 triple tick 2025-02-10 20:22:31 -08:00
Andrew Pareles e2e8279fce add back imports 2025-02-10 20:18:45 -08:00
Andrew Pareles 5ed1f465a2 MOVE VOID FILES TO WORKBENCH 2025-02-10 18:59:26 -08:00
Andrew Pareles 995357710e misc 2025-02-10 18:33:51 -08:00
Andrew Pareles a98106072c Merge remote-tracking branch 'origin/edit-chats' into model-selection 2025-02-10 15:35:37 -08:00
Andrew Pareles c1be466467 tool tests 2025-02-10 15:34:07 -08:00
Andrew Pareles 9cbe83b57c comment 2025-02-09 00:46:39 -08:00
Andrew Pareles e46b295449 names 2025-02-09 00:45:06 -08:00
Andrew Pareles 3fd3634257 void message 2025-02-09 00:36:03 -08:00
Andrew Pareles 231990088a uri 2025-02-09 00:28:44 -08:00
Andrew Pareles 20da82762e rm comment 2025-02-09 00:27:36 -08:00
Andrew Pareles 0d9be68aff rm comment 2025-02-09 00:27:21 -08:00
Andrew Pareles 165b5a4fd2 + 2025-02-09 00:26:58 -08:00
Andrew Pareles 812014d6d3 all tools seem to work! 2025-02-09 00:26:42 -08:00
Andrew Pareles d3e73b92e0 + 2025-02-08 22:56:55 -08:00
Andrew Pareles 00da05f24f context tools 2025-02-08 22:53:13 -08:00
Mathew Pareles e4694256eb Merge remote-tracking branch 'origin/model-selection' into edit-chats 2025-02-08 21:47:27 -08:00
Andrew Pareles 18b278577d tools 2025-02-08 21:17:38 -08:00
Mathew Pareles a252bb7a30 prepare merge 2025-02-08 17:26:28 -08:00
Mathew Pareles c240360710 edit update locally (need to test) 2025-02-08 17:14:16 -08:00
Mathew Pareles 893308c38e edits work 2025-02-07 02:06:04 -08:00
Mathew Pareles 48defe849d edit backend 2025-02-06 22:34:39 -08:00
Andrew Pareles 3c03f58488 clarify API 2025-02-06 22:16:42 -08:00
Mathew Pareles 2470e0de7f refactor 2025-02-06 19:01:15 -08:00
Mathew Pareles 387fd64db0 edit ability 2025-02-06 03:18:48 -08:00
Andrew Pareles 4ffd269db5 + 2025-02-06 03:11:26 -08:00
Andrew Pareles 5ce25e20c3 css for <html> md tags 2025-02-06 03:10:07 -08:00
Andrew Pareles 63efa219f5 div 2025-02-06 03:04:48 -08:00
Andrew Pareles 826d148305 html 2025-02-06 03:03:14 -08:00
Andrew Pareles 323d80ef55 add divs for html 2025-02-06 03:02:46 -08:00
Andrew Pareles 902588638d Merge remote-tracking branch 'origin/main' into model-selection 2025-02-06 02:04:48 -08:00
Andrew Pareles a1ab094652 + 2025-02-06 02:01:24 -08:00
Andrew Pareles 902b460b9c + 2025-02-06 02:00:33 -08:00
Andrew Pareles 073205ace3 warningbox and misc 2025-02-06 01:59:26 -08:00
Andrew Pareles 98c1d81875 focus() fix 2025-02-06 01:55:36 -08:00
Andrew Pareles 2f49650e15 model and provider error handling 2025-02-06 01:32:48 -08:00
Mathew Pareles 1be9300dc4 typeo 2025-02-05 18:19:04 -08:00
Andrew Pareles d9b77b67b2 prefix 2025-02-05 01:50:29 -08:00
Andrew Pareles 2f674f5323 update openai models 2025-02-05 01:10:40 -08:00
Andrew Pareles c8fa39e552 openai fim progress 2025-02-05 00:57:03 -08:00
Andrew Pareles 3b73e3ab26 metrics for sendFIM 2025-02-05 00:40:09 -08:00
Andrew Pareles c606ac4361 chat name 2025-02-05 00:38:49 -08:00
Andrew Pareles da4c9c8c40 fim better abstraction 2025-02-05 00:37:28 -08:00
Mathew Pareles 31ed11393f fix non-ollama autocomplete 2025-02-04 21:24:49 -08:00
Andrew Pareles 1c5157d97e remove hi333 2025-02-04 20:56:58 -08:00
Andrew Pareles cf241867d8 add recognizedModels code 2025-02-04 20:34:02 -08:00
Andrew Pareles 5a9f9f70d9 separate fastapply model 2025-02-04 20:34:02 -08:00
Andrew Pareles 19ff19fb80 Merge branch 'copy-icons' of https://github.com/voideditor/void into model-selection 2025-02-04 20:34:02 -08:00
Mathew Pareles 87076ed586 remove comments 2025-02-04 20:07:57 -08:00
Mathew Pareles 473250d8bc autocomplete works without context 2025-02-04 20:02:14 -08:00
Mathew Pareles cd983f0dd9 refactor input box with no noticable errors 2025-02-04 01:20:33 -08:00
Mathew Pareles 5c4753555e chatbubble add edit button (need to refactor InputBox to finish) 2025-02-03 21:06:29 -08:00
Mathew Pareles 8f79222b8b fix spacing 2025-02-03 18:45:25 -08:00
Mathew Pareles 145fd9614b changes 2025-02-03 18:27:57 -08:00
Mathew Pareles 41057c20fc autocomplete type context 2025-02-03 18:17:05 -08:00
Mathew Pareles 528b8a6b9b better model onboard 2025-02-03 18:17:05 -08:00
Andrew Pareles e3941d5565 + 2025-02-01 18:41:56 -08:00
Andrew Pareles 4d11a4fd29 Merge pull request #252 from bawsdeep/main
script to generate appimages
2025-01-31 20:52:44 -08:00
Andrew Pareles 3baaf8d6c4 comment 2025-01-31 20:24:09 -08:00
Andrew Pareles 0ec0c324b1 void settings opening behavior 2025-01-31 20:17:51 -08:00
Andrew Pareles aa580351d9 Merge pull request #259 from DoctorGlitchy/main
Update voidSettingsTypes.ts
2025-01-31 01:22:16 -08:00
Adrian Chan 570e13127d Update voidSettingsTypes.ts
Removed "distil-whisper-large-v3-en" because Whisper is not a model you chat with.
Added "llama3-70b-8192" + "mixtral-8x7b-32768"
2025-01-31 13:11:33 +11:00
Andrew Pareles fb7b50b36b Merge pull request #258 from voideditor/model-selection
Latest UI updates
2025-01-30 16:39:13 -08:00
Andrew Pareles 9cd8295dab fix comment 2025-01-30 16:38:51 -08:00
Andrew Pareles 9ec13d2ad8 rm comments 2025-01-30 16:37:44 -08:00
Andrew Pareles d9cf21448c add model selection + misc UI improvements (notably, change how model dropdown works) 2025-01-30 15:07:02 -08:00
Andrew Pareles 27d009314b finish merge 2025-01-29 15:55:18 -08:00
Andrew Pareles 5fdd21f12b Merge remote-tracking branch 'origin/main' into model-selection 2025-01-29 15:54:07 -08:00
Andrew Pareles dc5c7e9a46 Merge remote-tracking branch 'origin/main' into model-selection 2025-01-28 21:02:17 -08:00
Baws Deep d1c5cf518e Add files via upload 2025-01-29 05:41:12 +01:00
Andrew Pareles c1123f2cfd Merge pull request #233 from bjoaquinc/react-build-bug-fix
Fix npm run watchreact initial build bug
2025-01-28 20:04:51 -08:00
Andrew Pareles fb0bb0ca31 Merge remote-tracking branch 'origin/main' into pr/bjoaquinc/233 2025-01-28 20:04:11 -08:00
Andrew Pareles 4863802713 contrib 2025-01-28 20:02:57 -08:00
Andrew Pareles 5589b2e302 updates 2025-01-28 20:02:15 -08:00
Andrew Pareles 327dbb937b Merge pull request #183 from bjoaquinc/react-build-improvements
Improve React build by incorporating the watcher in base build commands
2025-01-28 19:56:05 -08:00
Mathew Pareles a3e62ee6f4 autocompletion multiline works 2025-01-28 01:43:47 -08:00
Andrew Pareles d7a6611d89 read 2025-01-28 01:23:40 -08:00
Mathew Pareles e4bb15ef64 autocomplete draft 2025-01-27 22:20:19 -08:00
Andrew Pareles a98c77572e add gap-4 and remove mt-4 for chat bubbles 2025-01-27 22:18:12 -08:00
Mathew Pareles e4d747d0a6 andrew ollamaFIM progress 2025-01-26 23:49:16 -08:00
Mathew Pareles 01b1358a01 autocomplete refactor 2025-01-26 19:01:03 -08:00
Mathew Pareles c7a6bddfbe groq 2025-01-26 19:01:03 -08:00
Andrew Pareles 55ea8e61c6 Merge pull request #190 from jcommaret/feat-mistral-new
[LLM]Mistral implementation
2025-01-25 19:40:44 -08:00
Andrew Pareles 4f7fd66c2f explanation 2025-01-25 19:39:38 -08:00
Andrew Pareles 4547363dde mistral fix 2025-01-25 19:38:55 -08:00
Andrew Pareles a1dfeb0f00 .tool-versions 2025-01-25 19:36:49 -08:00
Andrew Pareles b036a8c59c Merge remote-tracking branch 'origin/main' into pr/jcommaret/190 2025-01-25 19:35:05 -08:00
Andrew Pareles 72cd23f0ac Merge branch 'main' into pr/jcommaret/190 2025-01-25 19:31:25 -08:00
Andrew Pareles 49c2bcb848 Merge pull request #216 from Piyu-Pika/Gemini-experimental-model
Add new experimental Gemini models to default settings
2025-01-25 19:30:09 -08:00
Andrew Pareles 97ea0f77f9 remove duplicate model name and add storage fix 2025-01-25 19:29:22 -08:00
Andrew Pareles 0cd6e0fb67 ... 2025-01-25 19:01:47 -08:00
Andrew Pareles 7c227e237f key 2025-01-25 19:01:29 -08:00
Andrew Pareles 3089bb31b5 mistral 2025-01-25 18:39:37 -08:00
Jérôme Commaret 018fd016c8 trying to fix error 2025-01-26 02:05:58 +01:00
Mathew Pareles f69f90b4c8 Reset Window: Zoom Level default to 0 (#239)
Co-authored-by: Chandler Thompson <3262190+chandlertee@users.noreplyrgithub.com>
2025-01-25 16:37:00 -08:00
Jérôme Commaret 000747eb88 Merge branch 'main' into feat-mistral-new 2025-01-25 23:58:50 +01:00
Piyush Bhardwaj 74022e0306 Merge branch 'main' into Gemini-experimental-model 2025-01-25 19:42:54 +05:30
Andrew Pareles 21708fd7e0 contrib 2025-01-25 03:14:31 -08:00
Andrew Pareles 5593ff5d79 add key 2025-01-25 03:11:47 -08:00
Andrew Pareles 197067af5a contrib 2025-01-24 15:19:13 -08:00
Andrew Pareles eec4f6c95e email 2025-01-24 15:18:04 -08:00
Andrew Pareles 3e045117d6 contrib 2025-01-24 15:11:57 -08:00
Andrew Pareles 67c557b7b9 contributing 2025-01-24 15:02:16 -08:00
Joaquin Coromina 5ffd569fd8 Fixed npm run watch bug for first time builders by checking if src2 folder already exists, and if not then create it first before setting up the watchers 2025-01-25 00:02:26 +07:00
Jérôme Commaret ff83bb5edc Merge branch 'main' into feat-mistral-new and fix conflict introduced by Deepseek 2025-01-24 14:22:06 +01:00
Andrew Pareles a3f145328b Merge pull request #231 from voideditor/model-selection
DeepSeek as a provider
2025-01-24 03:24:46 -08:00
Mathew Pareles 02894c12de deepseek 2025-01-24 03:18:42 -08:00
Mathew Pareles 52b6c81507 update style of quickedit 2025-01-24 03:18:42 -08:00
Andrew Pareles 5df5f37a6a contributing 2025-01-24 01:12:12 -08:00
Andrew Pareles f247b76a6e Update README.md 2025-01-24 01:06:07 -08:00
Andrew Pareles d1c659be9d + 2025-01-24 01:05:31 -08:00
Andrew Pareles a6b4955f8d + 2025-01-23 21:58:48 -08:00
Andrew Pareles 03b0b98eed + 2025-01-23 21:58:26 -08:00
Andrew Pareles 7f7d30e40b + 2025-01-23 21:55:32 -08:00
Andrew Pareles f68bc52023 update readme 2025-01-23 21:42:55 -08:00
Andrew Pareles 520c971e3e remove jitter on load 2025-01-23 21:41:03 -08:00
Andrew Pareles 657e752987 fix autoupdate 2025-01-23 21:14:10 -08:00
Andrew Pareles 458a5160b0 metrics for updates 2025-01-23 20:58:07 -08:00
Andrew Pareles a90bbffeb9 Merge pull request #229 from voideditor/model-selection
Minor usability updates
2025-01-23 20:37:38 -08:00
Andrew Pareles 99ff885ce2 Merge remote-tracking branch 'origin/main' into model-selection 2025-01-23 20:34:35 -08:00
Andrew Pareles a97047ed43 higher code icon res 2025-01-23 20:34:10 -08:00
Andrew Pareles 00f10c283e update tracking 2025-01-23 20:11:15 -08:00
Andrew Pareles 57628ec244 rename version to vscodeVersion 2025-01-23 20:08:20 -08:00
Andrew Pareles c54dcd0b0c fix delta issue when streaming 2025-01-23 19:47:01 -08:00
Andrew Pareles e957932f39 avoid duplicate model listeners 2025-01-23 15:51:37 -08:00
Piyush Bhardwaj fea9d380cc Merge branch 'main' into Gemini-experimental-model 2025-01-23 22:35:17 +05:30
bjoaquinc f2318cc8a2 Merge branch 'voideditor:main' into react-build-improvements 2025-01-23 22:06:50 +07:00
Mathew Pareles 8264cccd04 change default theme to dark+. also set default zoom to -1 (window.zoomLevel.default) 2025-01-23 03:30:26 -08:00
Andrew Pareles a1ca0f9291 format 2025-01-23 03:25:00 -08:00
Andrew Pareles 58f06d3ebb minor changes 2025-01-23 02:55:29 -08:00
Mathew Pareles e06eda1cc4 wording 2025-01-23 02:28:24 -08:00
Mathew Pareles f4d72e3ee8 remove prompt spaces 2025-01-23 02:26:03 -08:00
Mathew Pareles b3837a3099 changes to prompt 2025-01-23 02:25:01 -08:00
Andrew Pareles c6ae6d5152 await whenReady 2025-01-23 01:36:45 -08:00
Andrew Pareles 2d43dbded1 write version 2025-01-23 00:18:13 -08:00
Andrew Pareles a34d652a1d improve prompts (+ change chat history types) 2025-01-23 00:02:50 -08:00
Andrew Pareles a0569b1ead better FIM codeblock matching 2025-01-23 00:02:50 -08:00
Andrew Pareles b3ebe85c04 rm system message 2025-01-23 00:02:50 -08:00
Mathew Pareles 0919775e77 refactors for autocomplete 2025-01-22 22:19:54 -08:00
Andrew Pareles da6636a948 update deviceId code 2025-01-22 18:13:48 -08:00
Mathew Pareles 6327c584cc remove role=system for compatibility 2025-01-22 17:23:02 -08:00
Andrew Pareles f7cf44136b loading 2025-01-22 16:57:02 -08:00
Andrew Pareles c8635a1bdb memo previous chat messages as we stream (important) 2025-01-22 16:02:34 -08:00
Andrew Pareles d7501112f5 fix error handling 2025-01-22 15:47:33 -08:00
Jérôme Commaret d7ca99e23d Merge branch 'main' into feat-mistral-new 2025-01-23 00:17:23 +01:00
Andrew Pareles 387be4db76 fix gemini 2025-01-22 03:25:27 -08:00
Andrew Pareles 2aec37fc57 numbering 2025-01-22 03:09:18 -08:00
Andrew Pareles 7e5cc27349 tes 2025-01-22 03:08:50 -08:00
Andrew Pareles 151cdb13e6 add roadmap 2025-01-22 02:56:56 -08:00
Andrew Pareles d54ccff4a2 see 2025-01-22 02:30:06 -08:00
Andrew Pareles c2f358bd5e #announcements 2025-01-22 01:18:50 -08:00
Andrew Pareles 198f04d87a minor 2025-01-22 01:04:08 -08:00
Andrew Pareles 326e75eb45 minor 2025-01-22 00:57:57 -08:00
Andrew Pareles 7891a908ff links 2025-01-22 00:55:22 -08:00
Andrew Pareles 6970ec9fd2 contrib 2025-01-22 00:54:01 -08:00
Mathew Pareles fd4f3c3ab6 Merge remote-tracking branch 'origin/main' into model-selection 2025-01-22 00:45:17 -08:00
Andrew Pareles aa2394e207 Update README.md 2025-01-22 00:38:26 -08:00
Andrew Pareles 3aa6d3d28a rm changelog 2025-01-21 23:15:44 -08:00
Andrew Pareles 98c1a7e528 minor change 2025-01-21 23:15:32 -08:00
Andrew Pareles c94dda65f7 Update CONTRIBUTING.md 2025-01-21 19:13:59 -08:00
Andrew Pareles 028061c879 Update CONTRIBUTING.md 2025-01-21 18:48:27 -08:00
Piyu-Pika cd9bb6a07d Add new experimental Gemini models to default settings 2025-01-21 21:46:02 +05:30
Jérôme Commaret fd989b2c13 Merge branch 'main' into feat-mistral-new 2025-01-20 16:12:45 +01:00
Andrew Pareles 362d481ec4 Merge pull request #215 from voideditor/model-selection
Beta!
2025-01-20 03:02:41 -08:00
Jérôme Commaret 4b8afc1771 Merge branch 'voideditor:main' into feat-mistral-new 2025-01-14 01:38:11 +01:00
Jérôme Commaret d067966b58 Merge branch 'model-selection' into feat-mistral-new 2025-01-13 23:31:19 +01:00
Jérôme Commaret 278c52bd35 Merging everything here 2025-01-13 22:48:43 +01:00
Jérôme Commaret 2701226ef3 Translate abort comments 2025-01-10 23:57:40 +01:00
Jérôme Commaret 9abc3ad3dc _aborter set 2025-01-10 23:46:58 +01:00
Jérôme Commaret af85949561 Merge branch 'main' into feat-mistral-new 2025-01-09 15:48:57 +01:00
Andrew Pareles 4e392bee8e start adding icons for copying files and folders in the explorer 2025-01-06 22:25:41 -08:00
Jérôme Commaret 5453acdb19 Mistral : update 2025-01-03 20:21:46 +01:00
Jérôme Commaret 7047459e57 Merge branch 'main' into feat-mistral-new 2025-01-03 19:57:34 +01:00
Jérôme Commaret 4065c5b504 Mistral implementation : Working 2024-12-27 16:37:27 +01:00
Jérôme Commaret 4729e4dac3 Mistral v2 2024-12-27 14:46:02 +01:00
Joaquin Coromina 1e975ecc3b Merge branch 'react-build-watcher' into react-build-improvements
Adding react build improvements to the proper branch#
2024-12-19 15:22:07 +08:00
Joaquin Coromina 8476a99903 Added watchreactd to tasks.json so that when using build hotkey (cmd/ctrl + shift + B) it includes watchreactd automatically 2024-12-19 15:10:42 +08:00
Joaquin Coromina 85314f8d47 Updated watch script to include watchreact, and added watchreactd 2024-12-19 15:09:16 +08:00
77 changed files with 7319 additions and 3548 deletions
+2
View File
@@ -22,3 +22,5 @@ product.overrides.json
*.snap.actual
.vscode-test
.tmp/
.tmp2/
.tool-versions
+30 -1
View File
@@ -29,6 +29,34 @@
}
}
},
{
"type": "npm",
"script": "watchreactd",
"label": "React - Build",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "buildWatchers",
"close": false
},
"problemMatcher": {
"owner": "typescript",
"applyTo": "closedDocuments",
"fileLocation": [
"absolute"
],
"pattern": {
"regexp": "Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$",
"file": 1,
"location": 2,
"message": 3
},
"background": {
"beginsPattern": "Starting compilation",
"endsPattern": "Finished compilation"
}
}
},
{
"type": "npm",
"script": "watch-extensionsd",
@@ -61,7 +89,8 @@
"label": "VS Code - Build",
"dependsOn": [
"Core - Build",
"Ext - Build"
"Ext - Build",
"React - Build"
],
"group": {
"kind": "build",
-35
View File
@@ -1,35 +0,0 @@
## Jan. 13, 2025 - Entering beta
- Added quick edits! Void handles FIM-prompting, output parsing, and history management for inline UI.
- Migrated away from VS Code extension API - Void now lives and interacts entirely within the VS Code codebase.
- New settings page with model configuration, one-click switch, and user settings.
- Added auto-detection (via polling) of local models by default.
- LLM requests originate from `node/`, which fixes common CORS and CSP issues when running some models locally.
- Misc improvements like UI and history for Accept | Reject in the sidebar and editor, streaming interruptions, and past chat history.
- Automatic file selection on tab switches.
- Lots of new UI, misc bug fixes, and performance improvements.
- VS Code's default Ctrl+L is now Ctrl+M in Void (on Mac Cmd+L becomes Cmd+M).
- Switched from the MIT License to the Apache 2.0 License. Apache's attribution clause provides a small amount of protection to our source initiative.
A huge shoutout to our many contributors. If you'd like to help build Void,
## Sept/Oct. 2024 - Early launch
- Initialized Void's website and GitHub repo.
- Started a waitlist.
+41 -30
View File
@@ -1,18 +1,29 @@
# Contributing to Void
Welcome! 👋 This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
### Welcome! 👋
This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
There are a few ways to contribute:
- 👨‍💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
- 💫 Complete items on the [Roadmap](https://github.com/orgs/voideditor/projects/2).
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension.
Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
- 🪴 Start new Issues - see [Issues](https://github.com/voideditor/void/issues).
## Building the full IDE
### Codebase Guide
We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization.
<!-- ADD BLOG HERE
We wrote a [guide to working in VSCode].
-->
Most of Void's code lives in the two folders called `void/`.
## Building Void
### a. Build Prerequisites - Mac
@@ -41,7 +52,7 @@ First, run `npm install -g node-gyp`. Then:
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
### Building Void
### d. Building Void
To build Void, open `void/` inside VSCode. Then open your terminal and run:
@@ -52,10 +63,11 @@ To build Void, open `void/` inside VSCode. Then open your terminal and run:
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
- This step can take ~5 min. The build is done when you see two check marks.
4. Run Void.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
- If you are actively developing Void, we strongly recommend adding the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command (just append them at the end of the string). This will save all data and extensions to the `.tmp` folder. You can delete this folder to reset any IDE changes you made when testing.
6. Nice-to-knows.
- You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload and see your new changes. It's faster than <kbd>Ctrl+Shift+P</kbd> and `Reload Window`.
- You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing.
#### Building Void from Terminal
@@ -70,20 +82,19 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
### Common Fixes
#### Common Fixes
- Make sure you follow the prerequisite steps.
- Make sure you have the same NodeJS version as `.nvmrc`.
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build.
- If you see missing styles, go to `src2/styles.css` and re-save the file.
- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`.
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
- Make sure you followed the prerequisite steps.
- Make sure you have Node version `20.16.0` (the version in `.nvmrc`)!
- 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.
## Bundling
## Packaging
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - compiling can take ~25 minutes.
We don't usually recommend packaging. Instead, you should probably just build. If you're sure you want to package Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - packaging can take ~25 minutes.
### Mac
@@ -109,17 +120,18 @@ workspace/
└── VSCode-darwin-arm64/ # Generated output
```
### 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.
## Pull Request Guidelines
# Guidelines
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs).
- Please submit a pull request once you've made a change.
- No need to submit an Issue unless you're creating a new feature that might involve multiple PRs.
- Please don't use AI to write your PR 🙂
## Submitting a Pull Request
- Please submit a pull request once you've made a change. No need to submit an Issue unless you're creating a new feature.
- Please don't use AI to write your PR 🙂.
<!--
@@ -127,6 +139,8 @@ We're always glad to talk about new ideas, help you get set up, and make sure yo
We keep track of all the files we've changed with Void so it's easy to rebase:
Edit: far too many changes to track... this is old
- README.md
- CONTRIBUTING.md
- VOID_USEFUL_LINKS.md
@@ -146,8 +160,5 @@ We keep track of all the files we've changed with Void so it's easy to rebase:
- build/npm/dirs.js
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
-->
## References
For some useful links we've compiled on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
+10 -10
View File
@@ -9,31 +9,31 @@
/>
</div>
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/) for downloading the official release. If you're new, welcome!
Void is the open-source Cursor alternative.
This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/email) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome!
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
- 📝 [Changelog](https://voideditor.com/changelog)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
<!-- ❤️ Setup: -->
## Contributing
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
1. To contribute to the Void repository, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
2. To suggest a new feature, please create a new [GitHub Issue](https://github.com/voideditor/void/issues). Suggestions will be added to our Roadmap within one day.
2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
3. We're open to collaborations and suggestions of all types - just reach out.
3. We're open to collaborations of all types. Feel free to contact us via email or discord.
## 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).
## Support
Feel free to reach out in our discord or contact us via email.
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
set -e # Exit on error
set -x # Print commands as they are executed
# Configuration
APP_NAME="void"
APP_VERSION="1.0.0"
ARCH="x86_64"
export ARCH
# Check if void binary exists in current directory
if [ ! -f "./void" ]; then
echo "Error: void binary not found in current directory"
exit 1
fi
# Check if icon exists
if [ ! -f "./void.png" ]; then
echo "Error: void.png icon not found in current directory"
exit 1
fi
# Create temporary directory
TEMP_DIR="$(mktemp -d)"
echo "Created temporary directory: $TEMP_DIR"
APP_DIR="$TEMP_DIR/$APP_NAME.AppDir"
# Create basic AppDir structure
mkdir -pv "$APP_DIR/usr/bin"
mkdir -pv "$APP_DIR/usr/lib"
mkdir -pv "$APP_DIR/usr/share/applications"
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
# Copy the icon to required locations
cp -v ./void.png "$APP_DIR/void.png"
cp -v ./void.png "$APP_DIR/usr/share/icons/hicolor/256x256/apps/void.png"
# Copy dependencies with error checking
echo "Copying dependencies..."
for lib in $(ldd ./void | grep "=> /" | awk '{print $3}'); do
if [ -f "$lib" ]; then
cp -v "$lib" "$APP_DIR/usr/lib/" || echo "Failed to copy $lib"
else
echo "Warning: Library $lib not found"
fi
done
# Create desktop file with error checking
echo "Creating desktop file..."
if ! cat > "$APP_DIR/$APP_NAME.desktop" <<EOF
[Desktop Entry]
Name=$APP_NAME
Exec=void
Icon=void
Type=Application
Categories=Utility;
Comment=Void Linux Application
EOF
then
echo "Error creating desktop file"
exit 1
fi
# Make desktop file executable
chmod +x "$APP_DIR/$APP_NAME.desktop"
# Copy the desktop file to the applications directory
cp -v "$APP_DIR/$APP_NAME.desktop" "$APP_DIR/usr/share/applications/"
# Create AppRun with error checking
echo "Creating AppRun..."
if ! cat > "$APP_DIR/AppRun" <<EOF
#!/bin/bash
cd "\$(dirname "\$0")/usr/bin"
export LD_LIBRARY_PATH="\$APPDIR/usr/lib:\$LD_LIBRARY_PATH"
exec ./void "\$@"
EOF
then
echo "Error creating AppRun"
exit 1
fi
# Make AppRun executable
chmod +x "$APP_DIR/AppRun"
# Download appimagetool if not present in the current directory
if [ ! -f "./appimagetool-x86_64.AppImage" ]; then
echo "Downloading appimagetool-x86_64.AppImage..."
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool-x86_64.AppImage
else
echo "appimagetool-x86_64.AppImage is already present."
fi
# Create the AppImage
echo "Creating AppImage..."
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APP_DIR" "${APP_NAME}-${APP_VERSION}-${ARCH}.AppImage"
# Cleanup
echo "Cleaning up..."
rm -rf "$TEMP_DIR"
echo "AppImage creation complete!"
+119 -40
View File
@@ -11,9 +11,11 @@
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@floating-ui/react": "^0.27.3",
"@google/generative-ai": "^0.21.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@mistralai/mistralai": "^1.4.0",
"@parcel/watcher": "2.1.0",
"@rrweb/record": "^2.0.0-alpha.17",
"@rrweb/types": "^2.0.0-alpha.17",
@@ -65,7 +67,8 @@
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "9.1.0",
"yauzl": "^3.0.0",
"yazl": "^2.4.3"
"yazl": "^2.4.3",
"zod": "^3.24.1"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
@@ -1549,6 +1552,65 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.3.tgz",
"integrity": "sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.9",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react/node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@google/generative-ai": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
@@ -2366,17 +2428,25 @@
"exenv-es6": "^1.1.1"
}
},
"node_modules/@mistralai/mistralai": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz",
"integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==",
"peerDependencies": {
"zod": ">= 3"
}
},
"node_modules/@next/env": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz",
"integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz",
"integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz",
"integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz",
"integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==",
"cpu": [
"arm64"
],
@@ -2391,9 +2461,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz",
"integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz",
"integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==",
"cpu": [
"x64"
],
@@ -2408,9 +2478,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz",
"integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz",
"integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==",
"cpu": [
"arm64"
],
@@ -2425,9 +2495,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz",
"integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz",
"integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==",
"cpu": [
"arm64"
],
@@ -2442,9 +2512,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz",
"integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz",
"integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==",
"cpu": [
"x64"
],
@@ -2459,9 +2529,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz",
"integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz",
"integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==",
"cpu": [
"x64"
],
@@ -2476,9 +2546,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz",
"integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz",
"integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==",
"cpu": [
"arm64"
],
@@ -2493,9 +2563,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz",
"integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz",
"integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==",
"cpu": [
"x64"
],
@@ -16219,13 +16289,13 @@
"dev": true
},
"node_modules/next": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz",
"integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz",
"integrity": "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/env": "15.1.4",
"@next/env": "15.1.6",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
@@ -16240,14 +16310,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.1.4",
"@next/swc-darwin-x64": "15.1.4",
"@next/swc-linux-arm64-gnu": "15.1.4",
"@next/swc-linux-arm64-musl": "15.1.4",
"@next/swc-linux-x64-gnu": "15.1.4",
"@next/swc-linux-x64-musl": "15.1.4",
"@next/swc-win32-arm64-msvc": "15.1.4",
"@next/swc-win32-x64-msvc": "15.1.4",
"@next/swc-darwin-arm64": "15.1.6",
"@next/swc-darwin-x64": "15.1.6",
"@next/swc-linux-arm64-gnu": "15.1.6",
"@next/swc-linux-arm64-musl": "15.1.6",
"@next/swc-linux-x64-gnu": "15.1.6",
"@next/swc-linux-x64-musl": "15.1.6",
"@next/swc-win32-arm64-msvc": "15.1.6",
"@next/swc-win32-x64-msvc": "15.1.6",
"sharp": "^0.33.5"
},
"peerDependencies": {
@@ -24104,6 +24174,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+6 -2
View File
@@ -14,6 +14,7 @@
"scripts": {
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
"watchreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js --watch && cd ../../../../../../../",
"watchreactd": "deemon npm run watchreact",
"test": "echo Please run any of the test scripts from the scripts folder.",
"test-browser": "npx playwright install && node test/unit/browser/index.js",
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
@@ -25,7 +26,7 @@
"preinstall": "node build/npm/preinstall.js",
"postinstall": "node build/npm/postinstall.js",
"compile": "node ./node_modules/gulp/bin/gulp.js compile",
"watch": "npm-run-all -lp watch-client watch-extensions",
"watch": "npm-run-all -lp watchreact watch-client watch-extensions",
"watch-amd": "npm-run-all -lp watch-client-amd watch-extensions",
"watchd": "deemon npm run watch",
"watch-webd": "deemon npm run watch-web",
@@ -78,9 +79,11 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@floating-ui/react": "^0.27.3",
"@google/generative-ai": "^0.21.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@mistralai/mistralai": "^1.4.0",
"@parcel/watcher": "2.1.0",
"@rrweb/record": "^2.0.0-alpha.17",
"@rrweb/types": "^2.0.0-alpha.17",
@@ -132,7 +135,8 @@
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "9.1.0",
"yauzl": "^3.0.0",
"yazl": "^2.4.3"
"yazl": "^2.4.3",
"zod": "^3.24.1"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
+7
View File
@@ -29,6 +29,7 @@
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"cookie": "^0.4.0",
"debounced": "1.0.2",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.3",
@@ -396,6 +397,12 @@
"node": ">= 0.6"
}
},
"node_modules/debounced": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/debounced/-/debounced-1.0.2.tgz",
"integrity": "sha512-6GPv+l/OOtdb1DKNY70k5ubuJhVjtBjUnujC5vQAHHrMuvBpDXsTc91xEMTdeA3/v4swYHamtdB9XIN7DcKxpw==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+1
View File
@@ -26,6 +26,7 @@
"cookie": "^0.4.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"debounced": "1.0.2",
"jschardet": "3.1.3",
"kerberos": "2.1.1",
"minimist": "^1.2.6",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 173 KiB

+5 -5
View File
@@ -121,11 +121,11 @@ import { normalizeNFC } from '../../base/common/normalization.js';
import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
import { IMetricsService } from '../../platform/void/common/metricsService.js';
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js';
import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js';
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/llmMessageChannel.js';
import { IMetricsService } from '../../workbench/contrib/void/common/metricsService.js';
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpdateService.js';
/**
* The main VS Code application. There will only ever be one instance,
* even if the user starts many instances (e.g. from the command line).
@@ -1,179 +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 { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { createDecorator } from '../../instantiation/common/instantiation.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');
export interface ILLMMessageService {
readonly _serviceBrand: undefined;
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
abort: (requestId: string) => void;
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
}
export class LLMMessageService extends Disposable implements ILLMMessageService {
readonly _serviceBrand: undefined;
private readonly channel: IChannel // LLMMessageChannel
// llmMessage
private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {}
private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {}
private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {}
// ollamaList
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) } = {}
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
// openAICompatibleList
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) } = {}
constructor(
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
// @INotificationService private readonly notificationService: INotificationService,
) {
super()
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
// see llmMessageChannel.ts
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
// llm
this._register((this.channel.listen('onText_llm') satisfies Event<EventLLMMessageOnTextParams>)(e => {
this.onTextHooks_llm[e.requestId]?.(e)
}))
this._register((this.channel.listen('onFinalMessage_llm') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
this.onFinalMessageHooks_llm[e.requestId]?.(e)
this._onRequestIdDone(e.requestId)
}))
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
console.error('Error in LLMMessageService:', JSON.stringify(e))
this.onErrorHooks_llm[e.requestId]?.(e)
this._onRequestIdDone(e.requestId)
}))
// ollama .list()
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
this.onSuccess_ollama[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
this.onError_ollama[e.requestId]?.(e)
}))
// openaiCompatible .list()
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
this.onSuccess_openAICompatible[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
this.onError_openAICompatible[e.requestId]?.(e)
}))
}
sendLLMMessage(params: ServiceSendLLMMessageParams) {
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { useProviderFor: featureName } = proxyParams
// end early if no provider
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
if (modelSelection === null) {
onError({ message: 'Please add a Provider in Settings!', fullError: null })
return null
}
const { providerName, modelName } = modelSelection
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
if (aiInstructions)
proxyParams.messages.unshift({ role: 'system', content: aiInstructions })
// add state for request id
const requestId_ = generateUuid();
this.onTextHooks_llm[requestId_] = onText
this.onFinalMessageHooks_llm[requestId_] = onFinalMessage
this.onErrorHooks_llm[requestId_] = onError
const { settingsOfProvider } = this.voidSettingsService.state
// params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', {
...proxyParams,
requestId: requestId_,
providerName,
modelName,
settingsOfProvider,
} satisfies MainLLMMessageParams);
return requestId_
}
abort(requestId: string) {
this.channel.call('abort', { requestId } satisfies MainLLMMessageAbortParams);
this._onRequestIdDone(requestId)
}
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
// add state for request id
const requestId_ = generateUuid();
this.onSuccess_ollama[requestId_] = onSuccess
this.onError_ollama[requestId_] = onError
this.channel.call('ollamaList', {
...proxyParams,
settingsOfProvider,
requestId: requestId_,
} satisfies MainModelListParams<OllamaModelResponse>)
}
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
// add state for request id
const requestId_ = generateUuid();
this.onSuccess_openAICompatible[requestId_] = onSuccess
this.onError_openAICompatible[requestId_] = onError
this.channel.call('openAICompatibleList', {
...proxyParams,
settingsOfProvider,
requestId: requestId_,
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
}
_onRequestIdDone(requestId: string) {
delete this.onTextHooks_llm[requestId]
delete this.onFinalMessageHooks_llm[requestId]
delete this.onErrorHooks_llm[requestId]
delete this.onSuccess_ollama[requestId]
delete this.onError_ollama[requestId]
}
}
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
@@ -1,412 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
export type VoidModelInfo = {
modelName: string,
isDefault: boolean, // whether or not it's a default for its provider
isHidden: boolean, // whether or not the user is hiding it
isAutodetected?: boolean, // whether the model was autodetected by polling
}
// creates `modelInfo` from `modelNames`
export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
const { isAutodetected, existingModels } = options ?? {}
if (!existingModels) { // default settings
return modelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: isAutodetected,
isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
}))
} else { // settings if there are existing models (keep existing `isHidden` property)
const existingModelsMap: Record<string, VoidModelInfo> = {}
for (const existingModel of existingModels) {
existingModelsMap[existingModel.modelName] = existingModel
}
return modelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: isAutodetected,
isHidden: !!existingModelsMap[modelName]?.isHidden,
}))
}
}
// https://docs.anthropic.com/en/docs/about-claude/models
export const defaultAnthropicModels = modelInfoOfDefaultNames([
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
// 'claude-3-haiku-20240307',
])
// https://platform.openai.com/docs/models/gp
export const defaultOpenAIModels = modelInfoOfDefaultNames([
'o1-preview',
'o1-mini',
'gpt-4o',
'gpt-4o-mini',
// 'gpt-4o-2024-05-13',
// 'gpt-4o-2024-08-06',
// 'gpt-4o-mini-2024-07-18',
// 'gpt-4-turbo',
// 'gpt-4-turbo-2024-04-09',
// 'gpt-4-turbo-preview',
// 'gpt-4-0125-preview',
// 'gpt-4-1106-preview',
// 'gpt-4',
// 'gpt-4-0613',
// 'gpt-3.5-turbo-0125',
// 'gpt-3.5-turbo',
// 'gpt-3.5-turbo-1106',
])
// https://console.groq.com/docs/models
export const defaultGroqModels = modelInfoOfDefaultNames([
"mixtral-8x7b-32768",
"llama2-70b-4096",
"gemma-7b-it"
])
export const defaultGeminiModels = modelInfoOfDefaultNames([
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-1.0-pro'
])
// export const parseMaxTokensStr = (maxTokensStr: string) => {
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
// if (Number.isNaN(int))
// return undefined
// return int
// }
export const anthropicMaxPossibleTokens = (modelName: string) => {
if (modelName === 'claude-3-5-sonnet-20241022'
|| modelName === 'claude-3-5-haiku-20241022')
return 8192
if (modelName === 'claude-3-opus-20240229'
|| modelName === 'claude-3-sonnet-20240229'
|| modelName === 'claude-3-haiku-20240307')
return 4096
return 1024 // return a reasonably small number if they're using a different model
}
type UnionOfKeys<T> = T extends T ? keyof T : never;
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
openAI: {
apiKey: '',
},
ollama: {
endpoint: 'http://127.0.0.1:11434',
},
openRouter: {
apiKey: '',
},
openAICompatible: {
endpoint: '',
apiKey: '',
},
gemini: {
apiKey: '',
},
groq: {
apiKey: ''
}
} as const
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
export const localProviderNames = ['ollama'] satisfies ProviderName[] // all local names
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
}
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
}
type CommonProviderSettings = {
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
}
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
[providerName in ProviderName]: SettingsForProvider<providerName>
}
export type SettingName = keyof SettingsForProvider<ProviderName>
type DisplayInfoForProviderName = {
title: string,
desc?: string,
}
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
if (providerName === 'anthropic') {
return {
title: 'Anthropic',
}
}
else if (providerName === 'openAI') {
return {
title: 'OpenAI',
}
}
else if (providerName === 'openRouter') {
return {
title: 'OpenRouter',
}
}
else if (providerName === 'ollama') {
return {
title: 'Ollama',
}
}
else if (providerName === 'openAICompatible') {
return {
title: 'OpenAI-Compatible',
}
}
else if (providerName === 'gemini') {
return {
title: 'Gemini',
}
}
else if (providerName === 'groq') {
return {
title: 'Groq',
}
}
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
type DisplayInfo = {
title: string,
placeholder: string,
subTextMd?: string,
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
if (settingName === 'apiKey') {
return {
title: 'API Key',
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
providerName === 'openAI' ? 'sk-proj-key...' :
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
providerName === 'gemini' ? 'key...' :
providerName === 'groq' ? 'gsk_key...' :
providerName === 'openAICompatible' ? 'sk-key...' :
'(never)',
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
providerName === 'openAICompatible' ? 'Add any OpenAI-Compatible endpoint.' :
undefined,
}
}
else if (settingName === 'endpoint') {
return {
title: providerName === 'ollama' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
: '(never)',
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
undefined,
}
}
else if (settingName === '_enabled') {
return {
title: '(never)',
placeholder: '(never)',
}
}
else if (settingName === 'models') {
return {
title: '(never)',
placeholder: '(never)',
}
}
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
}
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
}
export const voidInitModelOptions = {
anthropic: {
models: defaultAnthropicModels,
},
openAI: {
models: defaultOpenAIModels,
},
ollama: {
models: [],
},
openRouter: {
models: [], // any string
},
openAICompatible: {
models: [],
},
gemini: {
models: defaultGeminiModels,
},
groq: {
models: defaultGroqModels,
},
}
// used when waiting and for a type reference
export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: {
_enabled: undefined,
...defaultCustomSettings,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
},
openAI: {
_enabled: undefined,
...defaultCustomSettings,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
},
gemini: {
...defaultCustomSettings,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
_enabled: undefined,
},
groq: {
...defaultCustomSettings,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
_enabled: undefined,
},
ollama: {
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
_enabled: undefined,
},
openRouter: {
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
_enabled: undefined,
},
openAICompatible: {
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
_enabled: undefined,
},
}
export type ModelSelection = { providerName: ProviderName, modelName: string }
export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => {
return m1.modelName === m2.modelName && m1.providerName === m2.providerName
}
// this is a state
export type ModelSelectionOfFeature = {
'Ctrl+L': ModelSelection | null,
'Ctrl+K': ModelSelection | null,
'Autocomplete': ModelSelection | null,
}
export type FeatureName = keyof ModelSelectionOfFeature
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = localProviderNames
export type RefreshableProviderName = typeof refreshableProviderNames[number]
export type GlobalSettings = {
autoRefreshModels: boolean;
aiInstructions: string;
}
export const defaultGlobalSettings: GlobalSettings = {
autoRefreshModels: true,
aiInstructions: '',
}
export type GlobalSettingName = keyof GlobalSettings
export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[]
@@ -1,71 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import Anthropic from '@anthropic-ai/sdk';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
// Anthropic
type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
}
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = settingsOfProvider.anthropic
const maxTokens = anthropicMaxPossibleTokens(modelName)
if (maxTokens === undefined) {
onError({ message: `Please set a value for Max Tokens.`, fullError: null })
return
}
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
// find system messages and concatenate them
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// remove system messages for Anthropic
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
const stream = anthropic.messages.stream({
system: systemMessage,
messages: anthropicMessages,
model: modelName,
max_tokens: maxTokens,
});
// when receive text
stream.on('text', (newText, fullText) => {
onText({ newText, fullText })
})
// when we get the final message on this stream (or when error/fail)
stream.on('finalMessage', (claude_response) => {
// stringify the response's content
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
onFinalMessage({ fullText: content })
})
stream.on('error', (error) => {
// the most common error will be invalid API key (401), so we handle this with a nice message
if (error instanceof Anthropic.APIError && error.status === 401) {
onError({ message: 'Invalid API key.', fullError: error })
}
else {
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
}
})
// TODO need to test this to make sure it works, it might throw an error
_setAborter(() => stream.controller.abort())
};
@@ -1,53 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
// Gemini
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = ''
const thisConfig = settingsOfProvider.gemini
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
const model = genAI.getGenerativeModel({ model: modelName });
// remove system messages that get sent to Gemini
// str of all system messages
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// Convert messages to Gemini format
const geminiMessages: Content[] = messages
.filter(msg => msg.role !== 'system')
.map((msg, i) => ({
parts: [{ text: msg.content }],
role: msg.role === 'assistant' ? 'model' : 'user'
}))
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
.then(async response => {
_setAborter(() => response.stream.return(fullText))
for await (const chunk of response.stream) {
const newText = chunk.text();
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
.catch((error) => {
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
onError({ message: 'Invalid API key.', fullError: null });
}
else {
onError({ message: error + '', fullError: error });
}
})
}
@@ -1,68 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// // Greptile
// // https://docs.greptile.com/api-reference/query
// // https://docs.greptile.com/quickstart#sample-response-streamed
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
// let fullText = ''
// const thisConfig = settingsOfProvider.greptile
// fetch('https://api.greptile.com/v2/query', {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${thisConfig.apikey}`,
// 'X-Github-Token': `${thisConfig.githubPAT}`,
// 'Content-Type': `application/json`,
// },
// body: JSON.stringify({
// messages,
// stream: true,
// repositories: [thisConfig.repoinfo],
// }),
// })
// // this is {message}\n{message}\n{message}...\n
// .then(async response => {
// const text = await response.text()
// console.log('got greptile', text)
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
// })
// // TODO make this actually stream, right now it just sends one message at the end
// // TODO add _setAborter() when add streaming
// .then(async responseArr => {
// for (const response of responseArr) {
// const type: string = response['type']
// const message = response['message']
// // when receive text
// if (type === 'message') {
// fullText += message
// onText({ newText: message, fullText })
// }
// else if (type === 'sources') {
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
// fullText += filepath
// onText({ newText: filepath, fullText })
// }
// // type: 'status' with an empty 'message' means last message
// else if (type === 'status') {
// if (!message) {
// onFinalMessage({ fullText })
// }
// }
// }
// })
// .catch(error => {
// onError({ error })
// });
// }
@@ -1,46 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import Groq from 'groq-sdk';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
// Groq
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = '';
const thisConfig = settingsOfProvider.groq
const groq = new Groq({
apiKey: thisConfig.apiKey,
dangerouslyAllowBrowser: true
});
await groq.chat.completions
.create({
messages: messages,
model: modelName,
stream: true,
temperature: 0.7,
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
})
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
if (newText) {
fullText += newText;
onText({ newText, fullText });
}
}
onFinalMessage({ fullText });
})
.catch(error => {
onError({ message: error + '', fullError: error });
})
};
@@ -1,86 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Ollama } from 'ollama';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
onSuccess_({ models })
}
const onError = ({ error }: { error: string }) => {
onError_({ error })
}
try {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.list()
.then((response) => {
const { models } = response
onSuccess({ models })
})
.catch((error) => {
onError({ error: error + '' })
})
}
catch (error) {
onError({ error: error + '' })
}
}
// Ollama
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
let fullText = ''
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.chat({
model: modelName,
messages: messages,
stream: true,
// options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
})
.then(async stream => {
_setAborter(() => stream.abort())
// iterate through the stream
for await (const chunk of stream) {
const newText = chunk.message.content;
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
// when error/fail
.catch((error) => {
// if (typeof error === 'object') {
// const e = error.error as ErrorResponse['error']
// if (e) {
// const name = error.name ?? 'Error'
// onError({ error: `${name}: ${e}` })
// return;
// }
// }
onError({ message: error + '', fullError: error })
})
};
// ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',]
@@ -1,105 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import OpenAI from 'openai';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { Model } from 'openai/resources/models.js';
// import { parseMaxTokensStr } from './util.js';
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
const onSuccess = ({ models }: { models: Model[] }) => {
onSuccess_({ models })
}
const onError = ({ error }: { error: string }) => {
onError_({ error })
}
try {
const thisConfig = settingsOfProvider.openAICompatible
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
openai.models.list()
.then(async (response) => {
const models: Model[] = []
models.push(...response.data)
while (response.hasNextPage()) {
models.push(...(await response.getNextPage()).data)
}
onSuccess({ models })
})
.catch((error) => {
onError({ error: error + '' })
})
}
catch (error) {
onError({ error: error + '' })
}
}
// OpenAI, OpenRouter, OpenAICompatible
export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
let fullText = ''
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
if (providerName === 'openAI') {
const thisConfig = settingsOfProvider.openAI
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'openRouter') {
const thisConfig = settingsOfProvider.openRouter
openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
defaultHeaders: {
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'openAICompatible') {
const thisConfig = settingsOfProvider.openAICompatible
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else {
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
throw new Error(`providerName was invalid: ${providerName}`)
}
openai.models.list()
openai.chat.completions
.create(options)
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {
if (error instanceof OpenAI.APIError && error.status === 401) {
onError({ message: 'Invalid API key.', fullError: error });
}
else {
onError({ message: error, fullError: error });
}
})
};
@@ -1,114 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { sendAnthropicMsg } from './anthropic.js';
import { sendOllamaMsg } from './ollama.js';
import { sendOpenAIMsg } from './openai.js';
import { sendGeminiMsg } from './gemini.js';
import { sendGroqMsg } from './groq.js';
export const sendLLMMessage = ({
messages,
onText: onText_,
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
logging: { loggingName },
settingsOfProvider,
providerName,
modelName,
}: LLMMMessageParams,
metricsService: IMetricsService
) => {
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = (eventId: string, extras?: object) => {
metricsService.capture(eventId, {
providerName,
modelName,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
...extras,
})
}
const submit_time = new Date()
let _fullTextSoFar = ''
let _aborter: (() => void) | null = null
let _setAborter = (fn: () => void) => { _aborter = fn }
let _didAbort = false
const onText: OnText = ({ newText, fullText }) => {
if (_didAbort) return
onText_({ newText, fullText })
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText })
}
const onError: OnError = ({ message: error, fullError }) => {
if (_didAbort) return
console.error('sendLLMMessage onError:', error)
captureChatEvent(`${loggingName} - Error`, { error })
onError_({ message: error, fullError })
}
const onAbort = () => {
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
try { _aborter?.() } // aborter sometimes automatically throws an error
catch (e) { }
_didAbort = true
}
abortRef_.current = onAbort
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
try {
switch (providerName) {
case 'anthropic':
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'openAI':
case 'openRouter':
case 'openAICompatible':
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'gemini':
sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'groq':
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
default:
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
break;
}
}
catch (error) {
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
// ; (_aborter as any)?.()
// _didAbort = true
}
}
@@ -1,140 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// registered in app.ts
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
import { ollamaList } from './llmMessage/ollama.js';
import { openaiCompatibleList } from './llmMessage/openai.js';
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
export class LLMMessageChannel implements IServerChannel {
// sendLLMMessage
private readonly _onText_llm = new Emitter<EventLLMMessageOnTextParams>();
private readonly _onFinalMessage_llm = new Emitter<EventLLMMessageOnFinalMessageParams>();
private readonly _onError_llm = new Emitter<EventLLMMessageOnErrorParams>();
// abort
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
// ollamaList
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
// openaiCompatibleList
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
// stupidly, channels can't take in @IService
constructor(
private readonly metricsService: IMetricsService,
) { }
// browser uses this to listen for changes
listen(_: unknown, event: string): Event<any> {
if (event === 'onText_llm') {
return this._onText_llm.event;
}
else if (event === 'onFinalMessage_llm') {
return this._onFinalMessage_llm.event;
}
else if (event === 'onError_llm') {
return this._onError_llm.event;
}
else if (event === 'onSuccess_ollama') {
return this._onSuccess_ollama.event;
}
else if (event === 'onError_ollama') {
return this._onError_ollama.event;
}
else if (event === 'onSuccess_openAICompatible') {
return this._onSuccess_openAICompatible.event;
}
else if (event === 'onError_openAICompatible') {
return this._onError_openAICompatible.event;
}
else {
throw new Error(`Event not found: ${event}`);
}
}
// browser uses this to call
async call(_: unknown, command: string, params: any): Promise<any> {
try {
if (command === 'sendLLMMessage') {
this._callSendLLMMessage(params)
}
else if (command === 'abort') {
this._callAbort(params)
}
else if (command === 'ollamaList') {
this._callOllamaList(params)
}
else if (command === 'openAICompatibleList') {
this._callOpenAICompatibleList(params)
}
else {
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
}
}
catch (e) {
console.log('llmMessageChannel: Call Error:', e)
}
}
// the only place sendLLMMessage is actually called
private async _callSendLLMMessage(params: MainLLMMessageParams) {
const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId_llm))
this._abortRefOfRequestId_llm[requestId] = { current: null }
const mainThreadParams: LLMMMessageParams = {
...params,
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
abortRef: this._abortRefOfRequestId_llm[requestId],
}
sendLLMMessage(mainThreadParams, this.metricsService);
}
private _callAbort(params: MainLLMMessageAbortParams) {
const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId_llm)) return
this._abortRefOfRequestId_llm[requestId].current?.()
delete this._abortRefOfRequestId_llm[requestId]
}
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
const { requestId } = params;
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
...params,
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
}
ollamaList(mainThreadParams)
}
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
const { requestId } = params;
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
...params,
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
}
openaiCompatibleList(mainThreadParams)
}
}
@@ -1,100 +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 { Disposable } from '../../../base/common/lifecycle.js';
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
import { IProductService } from '../../product/common/productService.js';
import { IStorageMainService } from '../../storage/electron-main/storageMainService.js';
import { IMetricsService } from '../common/metricsService.js';
import { PostHog } from 'posthog-node'
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
const VOID_MACHINE_STORAGE_KEY = 'void.machineId'
export class MetricsMainService extends Disposable implements IMetricsService {
_serviceBrand: undefined;
private readonly client: PostHog
private readonly _initProperties: object
// TODO we should eventually identify people based on email
private get machineId() {
const currVal = this._storageService.applicationStorage.get(VOID_MACHINE_STORAGE_KEY)
if (currVal !== undefined) return currVal
const newVal = generateUuid()
this._storageService.applicationStorage.set(VOID_MACHINE_STORAGE_KEY, newVal)
return newVal
}
constructor(
@IProductService private readonly _productService: IProductService,
@IStorageMainService private readonly _storageService: IStorageMainService,
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
) {
super()
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', {
host: 'https://us.i.posthog.com',
})
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
const { commit, version, quality } = this._productService
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
// custom properties we identify
this._initProperties = {
commit,
version,
os,
quality,
distinctId: this.machineId,
isDevMode,
...this._getOSInfo(),
}
const identifyMessage = {
distinctId: this.machineId,
properties: this._initProperties,
}
this.client.identify(identifyMessage)
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
}
_getOSInfo() {
try {
const { platform, arch } = process // see platform.ts
return { platform, arch }
}
catch (e) {
return { osInfo: { platform: '??', arch: '??' } }
}
}
capture: IMetricsService['capture'] = (event, params) => {
const capture = { distinctId: this.machineId, event, properties: params } as const
// console.log('full capture:', capture)
this.client.capture(capture)
}
async getDebuggingProperties() {
return this._initProperties
}
}
@@ -14,6 +14,8 @@ import { WorkspaceEdit } from '../../../editor/common/languages.js';
// import { IHistoryService } from '../../services/history/common/history.js';
// VOID: THIS FILE IS OUTDATED!!!!!! No longer used anywhere.
@extHostNamedCustomer(MainContext.MainThreadInlineDiff)
export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape {
@@ -146,25 +146,6 @@ export class EditorGroupWatermark extends Disposable {
private render(): void {
// const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
// if (enabled === this.enabled) {
// return;
// }
// this.enabled = enabled;
// if (!enabled) {
// return;
// }
// const hasFolder = this.workbenchState !== WorkbenchState.EMPTY;
// const selected = (hasFolder ? folderEntries : noFolderEntries)
// .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
// .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
// .filter(entry => !!CommandsRegistry.getCommand(entry.id))
// .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
this.clear();
const voidIconBox = append(this.shortcuts, $('.watermark-box'));
@@ -176,6 +157,10 @@ export class EditorGroupWatermark extends Disposable {
const update = async () => {
// put async at top so don't need to wait (this prevents a jitter on load)
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
clearNode(voidIconBox);
clearNode(recentsBox);
@@ -206,10 +191,6 @@ export class EditorGroupWatermark extends Disposable {
// Recents
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
if (recentlyOpened.length !== 0) {
voidIconBox.append(
@@ -289,7 +270,7 @@ export class EditorGroupWatermark extends Disposable {
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
const button3 = append(recentsBox, $('button'));
button3.textContent = 'Void Settings'
button3.textContent = `Void Settings`
button3.style.display = 'block'
button3.style.marginLeft = 'auto'
button3.style.marginRight = 'auto'
@@ -289,6 +289,7 @@ export interface IFileTemplateData {
readonly templateDisposables: DisposableStore;
readonly elementDisposables: DisposableStore;
readonly label: IResourceLabel;
// readonly voidLabels: IResourceLabel;
readonly container: HTMLElement;
readonly contribs: IExplorerFileContribution[];
currentContext?: ExplorerItem;
@@ -347,15 +348,25 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
renderTemplate(container: HTMLElement): IFileTemplateData {
const templateDisposables = new DisposableStore();
// Void added this
// // Create void buttons container
// const voidButtonsContainer = DOM.append(container, DOM.$('div'));
// voidButtonsContainer.style.position = 'absolute'
// voidButtonsContainer.style.top = '0'
// voidButtonsContainer.style.right = '0'
// // const voidButtons = DOM.append(voidButtonsContainer, DOM.$('span'));
// // voidButtons.textContent = 'voidbuttons'
// // voidButtons.addEventListener('click', () => {
// // console.log('ON CLICK', templateData.currentContext?.children)
// // })
// const voidLabels = this.labels.create(voidButtonsContainer, { supportHighlights: false, supportIcons: false, });
// voidLabels.element.textContent = 'hi333'
const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true }));
templateDisposables.add(label.onDidRender(() => {
try {
if (templateData.currentContext) {
this.updateWidth(templateData.currentContext);
}
} catch (e) {
// noop since the element might no longer be in the tree, no update of width necessary
}
try { if (templateData.currentContext) this.updateWidth(templateData.currentContext); }
catch (e) { /* noop since the element might no longer be in the tree, no update of width necessary*/ }
}));
const contribs = explorerFileContribRegistry.create(this.instantiationService, container, templateDisposables);
@@ -365,10 +376,15 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
contr.setResource(templateData.currentContext?.resource);
}));
// const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, voidLabels, container, contribs };
const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, container, contribs };
return templateData;
}
// Void cares about this function, this is where elements in the tree are rendered
renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
const stat = node.element;
templateData.currentContext = stat;
@@ -382,8 +398,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
templateData.label.element.style.display = 'flex';
this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
}
// Input Box
// Input Box (Void - shown only if currently editing - this is the box that appears when user edits the name of the file)
else {
templateData.label.element.style.display = 'none';
templateData.contribs.forEach(c => c.setResource(undefined));
@@ -477,6 +492,13 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
domId
});
// templateData.voidLabels.setResource({ resource: undefined, name: 'hi', }, {
// hideIcon: true,
// extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
// forceLabel: true,
// });
}
private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
@@ -95,7 +95,12 @@ suite('Files - ExplorerView', () => {
label: <any>{
container: label,
onDidRender: emitter.event
}
},
// voidLabels: <any>{
// container: label,
// onDidRender: emitter.event
// },
}, 1, false);
ds.add(navigationController);
@@ -0,0 +1,135 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { Range } from '../../../../editor/common/core/range.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js';
import { URI } from '../../../../base/common/uri.js';
export interface IMarkerCheckService {
readonly _serviceBrand: undefined;
}
export const IMarkerCheckService = createDecorator<IMarkerCheckService>('markerCheckService');
class MarkerCheckService extends Disposable implements IMarkerCheckService {
_serviceBrand: undefined;
constructor(
@IMarkerService private readonly _markerService: IMarkerService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@ITextModelService private readonly _textModelService: ITextModelService,
) {
super();
setInterval(async () => {
const allMarkers = this._markerService.read();
const errors = allMarkers.filter(marker => marker.severity === MarkerSeverity.Error);
if (errors.length > 0) {
for (const error of errors) {
console.log(`----------------------------------------------`);
console.log(`${error.resource.toString()}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file
try {
// Get the text model for the file
const modelReference = await this._textModelService.createModelReference(error.resource);
const model = modelReference.object.textEditorModel;
// Create a range from the marker
const range = new Range(
error.startLineNumber,
error.startColumn,
error.endLineNumber,
error.endColumn
);
// Get code action providers for this model
const codeActionProvider = this._languageFeaturesService.codeActionProvider;
const providers = codeActionProvider.ordered(model);
if (providers.length > 0) {
// Request code actions from each provider
for (const provider of providers) {
const context: CodeActionContext = {
trigger: CodeActionTriggerType.Invoke, // keeping 'trigger' since it works
only: 'quickfix' // adding this to filter for quick fixes
};
const actions = await provider.provideCodeActions(
model,
range,
context,
CancellationToken.None
);
if (actions?.actions?.length) {
const quickFixes = actions.actions.filter(action => action.isPreferred); // ! all quickFixes for the error
const quickFixesForImports = actions.actions.filter(action => action.isPreferred && action.title.includes('import')); // ! all possible imports
quickFixesForImports
if (quickFixes.length > 0) {
console.log('Available Quick Fixes:');
quickFixes.forEach(action => {
console.log(`- ${action.title}`);
});
}
}
}
}
// Dispose the model reference
modelReference.dispose();
} catch (e) {
console.error('Error getting quick fixes:', e);
}
}
}
}, 5000);
}
fixErrorsInFiles(uris: URI[], contextSoFar: []) {
// const allMarkers = this._markerService.read();
// check errors in files
// give LLM errors in files
}
// private _onMarkersChanged = (changedResources: readonly URI[]): void => {
// for (const resource of changedResources) {
// const markers = this._markerService.read({ resource });
// if (markers.length === 0) {
// console.log(`${resource.toString()}: No diagnostics`);
// continue;
// }
// console.log(`Diagnostics for ${resource.toString()}:`);
// markers.forEach(marker => this._logMarker(marker));
// }
// };
}
registerSingleton(IMarkerCheckService, MarkerCheckService, InstantiationType.Eager);
@@ -0,0 +1,187 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
// import { URI } from '../../../../base/common/uri.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
// import { IToolService, ToolService } from '../common/toolsService.js';
export type ChatMessageLocation = {
threadId: string;
messageIdx: number;
}
export type SearchAndReplaceBlock = {
search: string;
replace: string;
}
// service that manages state
export type ApplyState = {
[applyBoxId: string]: {
searchAndReplaceBlocks: SearchAndReplaceBlock;
}
}
// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion`
export interface IFastApplyService {
readonly _serviceBrand: undefined;
// readonly state: ApplyState; // readonly to the user
// setState(newState: Partial<ApplyState>): void;
// onDidChangeState: Event<void>;
}
export const IVoidFastApplyService = createDecorator<IFastApplyService>('voidFastApplyService');
class VoidFastApplyService extends Disposable implements IFastApplyService {
_serviceBrand: undefined;
// static readonly ID = 'voidFastApplyService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
// state
// state: ApplyState
constructor(
// @IToolService private readonly toolService: ToolService
) {
super()
// initial state
// this.state = { currentUri: undefined }
}
setState(newState: Partial<ApplyState>) {
// this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
aiSearch(searchStr: string) {
}
aiReplace(searchStr: string, replaceStr: string) {
}
// 1. search(ai)
// - tool use to find all possible changes
// - if search only: is this file related to the search?
// - if search + replace: should I modify this file?
// 2. replace(ai)
// - what changes to make?
// 3. postprocess errors
// -fastapply changes simultaneously
// -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error)
// private async _searchUsingAI({ searchClause }: { searchClause: string }) {
// // const relevantURIs: URI[] = []
// // const gatherPrompt = `\
// // asdasdas
// // `
// // const filterPrompt = `\
// // Is this file relevant?
// // `
// // // optimizations (DO THESE LATER!!!!!!)
// // // if tool includes a uri in uriSet, skip it obviously
// // let uriSet = new Set<URI>()
// // // gather
// // let messages = []
// // while (true) {
// // const result = await new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search'],
// // onFinalMessage: ({ result: r, }) => {
// // res(r)
// // },
// // onError: (error) => {
// // rej(error)
// // }
// // })
// // })
// // messages.push({ role: 'tool', content: turnToString(result) })
// // sendLLMMessage({
// // messages: { 'Output ': result },
// // onFinalMessage: (r) => {
// // // output is file1\nfile2\nfile3\n...
// // }
// // })
// // uriSet.add(...)
// // }
// // // writes
// // if (!replaceClause) return
// // for (const uri of uriSet) {
// // // in future, batch these
// // applyWorkflow({ uri, applyStr: replaceClause })
// // }
// // while (true) {
// // const result = new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search'],
// // onResult: (r) => {
// // res(r)
// // }
// // })
// // })
// // messages.push(result)
// // }
// }
// private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) {
// for (const uri of relevantURIs) {
// uri
// }
// // should I change this file?
// // if so what changes to make?
// // fast apply the changes
// }
}
registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager);
@@ -5,19 +5,21 @@
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
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 { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Range } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { EditorResourceAccessor } from '../../../common/editor.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { _ln, allLinebreakSymbols } from '../common/voidFileService.js';
// import { IContextGatheringService } from './contextGatheringService.js';
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
@@ -134,17 +136,27 @@ class LRUCache<K, V> {
}
}
type AutocompletionStatus = 'pending' | 'finished' | 'error';
type AutocompletionPredictionType =
| 'single-line-fill-middle'
| 'single-line-redo-suffix'
// | 'multi-line-start-here'
| 'multi-line-start-on-next-line'
| 'do-not-predict'
type Autocompletion = {
id: number,
prefix: string,
suffix: string,
llmPrefix: string,
llmSuffix: string,
startTime: number,
endTime: number | undefined,
status: AutocompletionStatus,
status: 'pending' | 'finished' | 'error',
type: AutocompletionPredictionType,
llmPromise: Promise<string> | undefined,
insertText: string,
requestId: string | null,
_newlineCount: number,
}
const DEBOUNCE_TIME = 500
@@ -153,13 +165,16 @@ const MAX_CACHE_SIZE = 20
const MAX_PENDING_REQUESTS = 2
// postprocesses the result
const postprocessResult = (result: string) => {
const processStartAndEndSpaces = (result: string) => {
// trim all whitespace except for a single leading/trailing space
// return result.trim()
[result,] = extractCodeFromRegular({ text: result, recentlyAddedTextLen: result.length })
const hasLeadingSpace = result.startsWith(' ');
const hasTrailingSpace = result.endsWith(' ');
return (hasLeadingSpace ? ' ' : '')
+ result.trim()
+ (hasTrailingSpace ? ' ' : '');
@@ -168,13 +183,13 @@ const postprocessResult = (result: string) => {
// trims the end of the prefix to improve cache hit rate
const removeLeftTabsAndTrimEnd = (s: string): string => {
const removeLeftTabsAndTrimEnds = (s: string): string => {
const trimmedString = s.trimEnd();
const trailingEnd = s.slice(trimmedString.length);
// keep only a single trailing newline
if (trailingEnd.includes('\n')) {
s = trimmedString + '\n';
if (trailingEnd.includes(_ln)) {
s = trimmedString + _ln;
}
s = s.replace(/^\s+/gm, ''); // remove left tabs
@@ -184,7 +199,32 @@ const removeLeftTabsAndTrimEnd = (s: string): string => {
function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
const removeAllWhitespace = (str: string): string => str.replace(/\s+/g, '');
function getIsSubsequence({ of, subsequence }: { of: string, subsequence: string }): [boolean, string] {
if (subsequence.length === 0) return [true, ''];
if (of.length === 0) return [false, ''];
let subsequenceIndex = 0;
let lastMatchChar = '';
for (let i = 0; i < of.length; i++) {
if (of[i] === subsequence[subsequenceIndex]) {
lastMatchChar = of[i];
subsequenceIndex++;
}
if (subsequenceIndex === subsequence.length) {
return [true, lastMatchChar];
}
}
return [false, lastMatchChar];
}
function getStringUpToUnbalancedClosingParenthesis(s: string, prefix: string): string {
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
@@ -220,19 +260,14 @@ function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
}
const parenthesisChars = `{}()[]<>\`'"`
// further trim the autocompletion
const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => {
// returns the text in the autocompletion to display, assuming the prefix is already matched
const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, position, debug }: { matchInfo: matchInfo, prefix: string, suffix: string, autocompletion: Autocompletion, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
const { prefix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } = prefixAndSuffix
const suffixLines = suffix.split('\n')
const prefixLines = prefix.split('\n')
const suffixToTheRightOfCursor = suffixLines[0]
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1]
const generatedMiddle = autocompletion.insertText
let startIdx = matchInfo.startIdx
let startIdx = autocompletionMatchup.startIdx
let endIdx = generatedMiddle.length // exclusive bounds
// const naiveReturnValue = generatedMiddle.slice(startIdx)
@@ -253,7 +288,7 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
}
// if user is on a blank line and the generation starts with newline(s), remove them
const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0;
const numStartingNewlines = generatedMiddle.slice(startIdx).match(new RegExp(`^${_ln}+`))?.[0].length || 0;
if (
!prefixToTheLeftOfCursor.trim()
&& !suffixToTheRightOfCursor.trim()
@@ -263,21 +298,21 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
startIdx += numStartingNewlines
}
// if the generated text matches with the suffix on the current line, stop
if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
// if the generated FIM text matches with the suffix on the current line, stop
if (autocompletion.type === 'single-line-fill-middle' && suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
// complete until there is a match
const rawMatchIndex = generatedMiddle.slice(startIdx).lastIndexOf(suffixToTheRightOfCursor.trim()[0])
if (rawMatchIndex > -1) {
// console.log('p2', rawMatchIndex, startIdx, suffixToTheRightOfCursor.trim()[0], 'AAA', generatedMiddle.slice(startIdx))
const matchIdx = rawMatchIndex + startIdx;
const matchChar = generatedMiddle[matchIdx]
if (parenthesisChars.includes(matchChar)) {
if (`{}()[]<>\`'"`.includes(matchChar)) {
endIdx = Math.min(endIdx, matchIdx)
}
}
}
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? ''
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split(_ln)[0] ?? ''
// condition to complete as a single line completion
if (
prefixToTheLeftOfCursor.trim()
@@ -285,7 +320,7 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
&& restOfLineToGenerate.trim()
) {
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n')
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf(_ln)
if (rawNewlineIdx > -1) {
// console.log('p3', startIdx, rawNewlineIdx)
const newlineIdx = rawNewlineIdx + startIdx;
@@ -311,14 +346,48 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
let completionStr = generatedMiddle.slice(startIdx, endIdx)
// filter out unbalanced parentheses
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix)
completionStr = getStringUpToUnbalancedClosingParenthesis(completionStr, prefix)
// console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
// console.log('finalCompletionStr: ', JSON.stringify(completionStr))
return completionStr
}
// returns the text in the autocompletion to display, assuming the prefix is already matched
const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndSuffix, position, debug }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
let trimmedInsertText = postprocessAutocompletion({ autocompletionMatchup, autocompletion, prefixAndSuffix, })
let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column)
// handle special cases
// if we redid the suffix, replace the suffix
if (autocompletion.type === 'single-line-redo-suffix') {
const oldSuffix = prefixAndSuffix.suffixToTheRightOfCursor
const newSuffix = autocompletion.insertText
const [isSubsequence, lastMatchingChar] = getIsSubsequence({ // check that the old text contains the same brackets + symbols as the new text
subsequence: removeAllWhitespace(oldSuffix), // old suffix
of: removeAllWhitespace(newSuffix), // new suffix
})
if (isSubsequence) {
rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER)
}
else {
const lastMatchupIdx = trimmedInsertText.lastIndexOf(lastMatchingChar)
trimmedInsertText = trimmedInsertText.slice(0, lastMatchupIdx + 1)
const numCharsToReplace = oldSuffix.lastIndexOf(lastMatchingChar) + 1
rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, position.column + numCharsToReplace)
// console.log('show____', trimmedInsertText, rangeToReplace)
}
}
return [{
insertText: completionStr,
insertText: trimmedInsertText,
range: rangeToReplace,
}]
@@ -345,7 +414,9 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
// }
const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string }
const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => {
const fullText = model.getValue();
@@ -353,30 +424,37 @@ const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
const prefix = fullText.substring(0, cursorOffset)
const suffix = fullText.substring(cursorOffset)
return { prefix, suffix }
const prefixLines = prefix.split(_ln)
const suffixLines = suffix.split(_ln)
const prefixToTheLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
const suffixToTheRightOfCursor = suffixLines[0] ?? ''
return { prefix, suffix, prefixLines, suffixLines, prefixToTheLeftOfCursor, suffixToTheRightOfCursor }
}
const getIndex = (str: string, line: number, char: number) => {
return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char;
return str.split(_ln).slice(0, line).join(_ln).length + (line > 0 ? 1 : 0) + char;
}
const getLastLine = (s: string): string => {
const matches = s.match(/[^\n]*$/)
const matches = s.match(new RegExp(`[^${_ln}]*$`))
return matches ? matches[0] : ''
}
type matchInfo = {
lineStart: number,
character: number,
type AutocompletionMatchupBounds = {
startLine: number,
startCharacter: number,
startIdx: number,
}
// returns the startIdx of the match if there is a match, or undefined if there is no match
// all results are wrt `autocompletion.result`
const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => {
const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): AutocompletionMatchupBounds | undefined => {
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix)
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix)
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText)
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnds(prefix)
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnds(autocompletion.prefix)
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnds(autocompletion.insertText)
// console.log('@result: ', JSON.stringify(autocompletion.insertText))
// console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix))
@@ -384,7 +462,7 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
// console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle))
if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time
console.log('@undefined1')
// console.log('@undefined1')
return undefined
}
@@ -392,24 +470,24 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
!(trimmedCompletionPrefix + trimmedCompletionMiddle)
.startsWith(trimmedCurrentPrefix)
) {
console.log('@undefined2')
// console.log('@undefined2')
return undefined
}
// reverse map to find position wrt `autocompletion.result`
const lineStart =
trimmedCurrentPrefix.split('\n').length -
trimmedCompletionPrefix.split('\n').length;
trimmedCurrentPrefix.split(_ln).length -
trimmedCompletionPrefix.split(_ln).length;
if (lineStart < 0) {
console.log('@undefined3')
// console.log('@undefined3')
console.error('Error: No line found.');
return undefined;
}
const currentPrefixLine = getLastLine(trimmedCurrentPrefix)
const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : ''
const completionMiddleLine = autocompletion.insertText.split('\n')[lineStart]
const completionMiddleLine = autocompletion.insertText.split(_ln)[lineStart]
const fullCompletionLine = completionPrefixLine + completionMiddleLine
// console.log('currentPrefixLine', currentPrefixLine)
@@ -418,7 +496,7 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine)
if (charMatchIdx < 0) {
console.log('@undefined4', charMatchIdx)
// console.log('@undefined4', charMatchIdx)
console.error('Warning: Found character with negative index. This should never happen.')
return undefined
@@ -432,8 +510,8 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
const startIdx = getIndex(autocompletion.insertText, lineStart, character)
return {
lineStart,
character,
startLine: lineStart,
startCharacter: character,
startIdx,
}
@@ -441,40 +519,89 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
}
type CompletionOptions = {
predictionType: AutocompletionPredictionType,
shouldGenerate: boolean,
llmPrefix: string,
llmSuffix: string,
stopTokens: string[],
}
const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantContext: string, justAcceptedAutocompletion: boolean): CompletionOptions => {
let { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines, prefixLines } = prefixAndSuffix
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
// trim prefix and suffix to not be very large
suffixLines = suffix.split(_ln).slice(0, 25)
prefixLines = prefix.split(_ln).slice(-25)
prefix = prefixLines.join(_ln)
suffix = suffixLines.join(_ln)
const prefixLines = prefix.split('\n')
const suffixLines = suffix.split('\n')
let completionOptions: CompletionOptions
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
const suffixToRightOfCursor = suffixLines[0] ?? ''
// if line is empty, do multiline completion
const isLineEmpty = !prefixToTheLeftOfCursor.trim() && !suffixToTheRightOfCursor.trim()
const isLinePrefixEmpty = removeAllWhitespace(prefixToTheLeftOfCursor).length === 0
const isLineSuffixEmpty = removeAllWhitespace(suffixToTheRightOfCursor).length === 0
// default parameters
let shouldGenerate = true
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
// TODO add context to prefix
// llmPrefix = '\n\n/* Relevant context:\n' + relevantContext + '\n*/\n' + llmPrefix
// specific cases
if (suffixToRightOfCursor.trim() !== '') { // typing between something
stopTokens = ['\n', '\r\n']
// if we just accepted an autocompletion, predict a multiline completion starting on the next line
if (justAcceptedAutocompletion && isLineSuffixEmpty) {
const prefixWithNewline = prefix + _ln
completionOptions = {
predictionType: 'multi-line-start-on-next-line',
shouldGenerate: true,
llmPrefix: prefixWithNewline,
llmSuffix: suffix,
stopTokens: [`${_ln}${_ln}`] // double newlines
}
}
// if the current line is empty, predict a single-line completion
else if (isLineEmpty) {
completionOptions = {
predictionType: 'single-line-fill-middle',
shouldGenerate: true,
llmPrefix: prefix,
llmSuffix: suffix,
stopTokens: allLinebreakSymbols
}
}
// if suffix is 3 or fewer characters, attempt to complete the line ignorning it
else if (removeAllWhitespace(suffixToTheRightOfCursor).length <= 3) {
const suffixLinesIgnoringThisLine = suffixLines.slice(1)
const suffixStringIgnoringThisLine = suffixLinesIgnoringThisLine.length === 0 ? '' : _ln + suffixLinesIgnoringThisLine.join(_ln)
completionOptions = {
predictionType: 'single-line-redo-suffix',
shouldGenerate: true,
llmPrefix: prefix,
llmSuffix: suffixStringIgnoringThisLine,
stopTokens: allLinebreakSymbols
}
}
// else attempt to complete the middle of the line if there is a prefix (the completion looks bad if there is no prefix)
else if (!isLinePrefixEmpty) {
completionOptions = {
predictionType: 'single-line-fill-middle',
shouldGenerate: true,
llmPrefix: prefix,
llmSuffix: suffix,
stopTokens: allLinebreakSymbols
}
} else {
completionOptions = {
predictionType: 'do-not-predict',
shouldGenerate: false,
llmPrefix: prefix,
llmSuffix: suffix,
stopTokens: []
}
}
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
// stopTokens = ['\n\n', '\r\n\r\n']
// }
if (prefixToLeftOfCursor === '') { // at beginning or end of line
shouldGenerate = false
}
return { shouldGenerate, stopTokens }
return completionOptions
}
export interface IAutocompleteService {
readonly _serviceBrand: undefined;
}
@@ -482,13 +609,17 @@ export interface IAutocompleteService {
export const IAutocompleteService = createDecorator<IAutocompleteService>('AutocompleteService');
export class AutocompleteService extends Disposable implements IAutocompleteService {
static readonly ID = 'void.autocompleteService'
_serviceBrand: undefined;
private _autocompletionId: number = 0;
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
private _lastCompletionTime = 0
private _lastPrefix: string = ''
private _lastCompletionStart = 0
private _lastCompletionAccept = 0
// private _lastPrefix: string = ''
// used internally by vscode
// fires after every keystroke and returns the completion to show
@@ -499,16 +630,15 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
token: CancellationToken,
): Promise<InlineCompletion[]> {
const disabled = true
const testMode = false
if (disabled) return [];
const docUriStr = model.uri.toString();
const { prefix, suffix } = getPrefixAndSuffix(model, position)
// initialize cache and other variables
// note that whenever an autocompletion is rejected, it is removed from cache
const prefixAndSuffix = getPrefixAndSuffixInfo(model, position)
const { prefix, suffix } = prefixAndSuffix
// initialize cache if it doesnt exist
// note that whenever an autocompletion is accepted, it is removed from cache
if (!this._autocompletionsOfDocument[docUriStr]) {
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>(
MAX_CACHE_SIZE,
@@ -518,7 +648,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}
)
}
this._lastPrefix = prefix
// this._lastPrefix = prefix
// print all pending autocompletions
// let _numPending = 0
@@ -527,33 +657,36 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
// get autocompletion from cache
let cachedAutocompletion: Autocompletion | undefined = undefined
let matchInfo: matchInfo | undefined = undefined
let autocompletionMatchup: AutocompletionMatchupBounds | undefined = undefined
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
// if the user's change matches up with the generated text
matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion })
if (matchInfo !== undefined) {
// if the user's change matches with the autocompletion
autocompletionMatchup = getAutocompletionMatchup({ prefix, autocompletion })
if (autocompletionMatchup !== undefined) {
cachedAutocompletion = autocompletion
break;
}
}
// if there is a cached autocompletion, return it
if (cachedAutocompletion && matchInfo) {
if (cachedAutocompletion && autocompletionMatchup) {
console.log('AA')
// console.log('id: ' + cachedAutocompletion.id)
if (cachedAutocompletion.status === 'finished') {
// console.log('A1')
console.log('A1')
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true })
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position, debug: true })
return inlineCompletions
} else if (cachedAutocompletion.status === 'pending') {
// console.log('A2')
console.log('A2')
try {
await cachedAutocompletion.llmPromise;
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position })
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position })
return inlineCompletions
} catch (e) {
@@ -562,19 +695,25 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}
} else if (cachedAutocompletion.status === 'error') {
// console.log('A3')
console.log('A3')
} else {
console.log('A4')
}
return []
}
// else if no more typing happens, then go forwards with the request
// wait DEBOUNCE_TIME for the user to stop typing
const thisTime = Date.now()
this._lastCompletionTime = thisTime
const justAcceptedAutocompletion = thisTime - this._lastCompletionAccept < 500
this._lastCompletionStart = thisTime
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
setTimeout(() => {
if (this._lastCompletionTime === thisTime) {
if (this._lastCompletionStart === thisTime) {
resolve(false)
} else {
resolve(true)
@@ -605,7 +744,15 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}
}
const { shouldGenerate, stopTokens: _ } = getCompletionOptions({ prefix, suffix }) // TODO mat
// gather relevant context from the code around the user's selection and definitions
// const relevantSnippetsList = await this._contextGatheringService.readCachedSnippets(model, position, 3);
// const relevantSnippetsList = this._contextGatheringService.getCachedSnippets();
// const relevantSnippets = relevantSnippetsList.map((text) => `${text}`).join('\n-------------------------------\n')
// console.log('@@---------------------\n' + relevantSnippets)
const relevantContext = ''
const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantContext, justAcceptedAutocompletion)
if (!shouldGenerate) return []
@@ -613,47 +760,75 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
return []
}
// console.log('B')
// create a new autocompletion and add it to cache
const newAutocompletion: Autocompletion = {
id: this._autocompletionId++,
prefix: prefix,
prefix: prefix, // the actual prefix and suffix
suffix: suffix,
llmPrefix: llmPrefix, // the prefix and suffix the llm sees
llmSuffix: llmSuffix,
startTime: Date.now(),
endTime: undefined,
type: predictionType,
status: 'pending',
llmPromise: undefined,
insertText: '',
requestId: null,
_newlineCount: 0,
}
console.log('starting autocomplete...', predictionType)
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
logging: { loggingName: 'Autocomplete' },
messages: [],
onText: async ({ newText, fullText }) => {
newAutocompletion.insertText = fullText
// if generation doesn't match the prefix for the first few tokens generated, reject it
if (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
reject('LLM response did not match user\'s text.')
}
messagesType: 'FIMMessage',
messages: {
prefix: llmPrefix,
suffix: llmSuffix,
stopTokens: stopTokens,
},
useProviderFor: 'Autocomplete',
logging: { loggingName: 'Autocomplete' },
onText: () => { }, // unused in FIMMessage
// onText: async ({ fullText, newText }) => {
// newAutocompletion.insertText = fullText
// // count newlines in newText
// const numNewlines = newText.match(/\n|\r\n/g)?.length || 0
// newAutocompletion._newlineCount += numNewlines
// // if too many newlines, resolve up to last newline
// if (newAutocompletion._newlineCount > 10) {
// const lastNewlinePos = fullText.lastIndexOf('\n')
// newAutocompletion.insertText = fullText.substring(0, lastNewlinePos)
// resolve(newAutocompletion.insertText)
// return
// }
// // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
// // reject('LLM response did not match user\'s text.')
// // }
// },
onFinalMessage: ({ fullText }) => {
// newAutocompletion.prefix = prefix
// newAutocompletion.suffix = suffix
// newAutocompletion.startTime = Date.now()
// console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
newAutocompletion.endTime = Date.now()
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
// newAutocompletion.promise = undefined
const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 })
newAutocompletion.insertText = postprocessResult(text)
newAutocompletion.insertText = processStartAndEndSpaces(text)
// handle special case for predicting starting on the next line, add a newline character
if (newAutocompletion.type === 'multi-line-start-on-next-line') {
newAutocompletion.insertText = _ln + newAutocompletion.insertText
}
resolve(newAutocompletion.insertText)
@@ -663,8 +838,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(message)
},
useProviderFor: 'Autocomplete',
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
})
newAutocompletion.requestId = requestId
@@ -687,8 +860,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
await newAutocompletion.llmPromise
// console.log('id: ' + newAutocompletion.id)
const matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 }
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position })
const autocompletionMatchup: AutocompletionMatchupBounds = { startIdx: 0, startLine: 0, startCharacter: 0 }
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: newAutocompletion, prefixAndSuffix, position })
return inlineCompletions
} catch (e) {
@@ -704,6 +877,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
) {
super()
@@ -715,7 +889,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
return { items: items, }
},
freeInlineCompletions: (completions) => {
// get the `docUriStr` and the `position` of the cursor
const activePane = this._editorService.activeEditorPane;
if (!activePane) return;
@@ -728,33 +901,32 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
const model = this._modelService.getModel(resource)
if (!model) return;
const docUriStr = resource.toString();
const { prefix, } = getPrefixAndSuffix(model, position)
if (!this._autocompletionsOfDocument[docUriStr]) return;
const { prefix, } = getPrefixAndSuffixInfo(model, position)
// go through cached items and remove matching ones
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
completions.items.forEach(item => {
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
if (removeLeftTabsAndTrimEnd(prefix)
=== removeLeftTabsAndTrimEnd(autocompletion.prefix + autocompletion.insertText)
) {
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
}
});
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
// we can do this more efficiently, I just didn't want to deal with all of the edge cases
const matchup = removeAllWhitespace(prefix) === removeAllWhitespace(autocompletion.prefix + autocompletion.insertText)
if (matchup) {
console.log('ACCEPT', autocompletion.id)
this._lastCompletionAccept = Date.now()
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
}
});
},
})
}
}
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
registerWorkbenchContribution2(AutocompleteService.ID, AutocompleteService, WorkbenchPhase.BlockRestore);
@@ -11,50 +11,82 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { VSReadFile } from './helpers/readFile.js';
import { chat_prompt, chat_systemMessage } from './prompt/prompts.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from './prompt/prompts.js';
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js';
import { toLLMChatMessage } from '../common/llmMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IVoidFileService } from '../common/voidFileService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
for (let i = arr.length - 1; i >= 0; i--) {
if (condition(arr[i])) {
return i;
}
}
return -1;
}
// 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;
selectionStr: string | null;
content: string; // TODO remove this (replace `selectionStr` with `content`)
selectionStr: string;
range: IRange;
}
// if selectionStr is null, it means to use the entire file at send time
export type CodeStagingSelection = {
type: 'Selection',
fileURI: URI,
selectionStr: string,
range: IRange
} | {
type: 'File',
fileURI: URI,
selectionStr: null,
range: null
export type FileSelection = {
type: 'File';
fileURI: URI;
selectionStr: null;
range: null;
}
export type StagingSelectionItem = CodeSelection | FileSelection
export type ToolMessage<T extends ToolName> = {
role: 'tool';
name: T; // internal use
params: string; // internal use
id: string; // apis require this tool use id
content: string; // result
result: ToolCallReturnType[T]; // text message of result
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
role: 'system';
content: string;
displayContent?: undefined;
} | {
role: 'user';
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
selections: CodeSelection[] | null; // the user's selection
}
| {
selections: StagingSelectionItem[] | null; // the user's selection
state: {
stagingSelections: StagingSelectionItem[];
isBeingEdited: boolean;
}
} | {
role: 'assistant';
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
}
| {
role: 'system';
content: string;
displayContent?: undefined;
}
| ToolMessage<ToolName>
type UserMessageType = ChatMessage & { role: 'user' }
type UserMessageState = UserMessageType['state']
export const defaultMessageState: UserMessageState = {
stagingSelections: [],
isBeingEdited: false,
}
// a 'thread' means a chat message history
export type ChatThreads = {
@@ -63,18 +95,26 @@ export type ChatThreads = {
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
state: {
stagingSelections: StagingSelectionItem[];
focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none)
isCheckedOfSelectionId: { [selectionId: string]: boolean };
}
};
}
type ThreadType = ChatThreads[string]
const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} }
export type ThreadsState = {
allThreads: ChatThreads;
currentThreadId: string; // intended for internal use only
currentStagingSelections: CodeStagingSelection[] | null;
}
export type ThreadStreamState = {
[threadId: string]: undefined | {
error?: { message: string, fullError: Error | null };
error?: { message: string, fullError: Error | null, };
messageSoFar?: string;
streamingToken?: string;
}
@@ -84,15 +124,26 @@ export type ThreadStreamState = {
const newThreadObject = () => {
const now = new Date().toISOString()
return {
id: new Date().getTime().toString(),
id: generateUuid(),
createdAt: now,
lastModified: now,
messages: [],
state: {
stagingSelections: [],
focusedMessageIdx: undefined,
isCheckedOfSelectionId: {}
},
} satisfies ChatThreads[string]
}
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
const LATEST_THREAD_VERSION = 'v2'
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
type ChatMode = 'agent' | 'chat'
export interface IChatThreadService {
readonly _serviceBrand: undefined;
@@ -106,9 +157,25 @@ export interface IChatThreadService {
openNewThread(): void;
switchToThread(threadId: string): void;
setStaging(stagingSelection: CodeStagingSelection[] | null): void;
// you can edit multiple messages
// the one you're currently editing is "focused", and we add items to that one when you press cmd+L.
getFocusedMessageIdx(): number | undefined;
isFocusingMessage(): boolean;
setFocusedMessageIdx(messageIdx: number | undefined): void;
// exposed getters/setters
getCurrentMessageState: (messageIdx: number) => UserMessageState
setCurrentMessageState: (messageIdx: number, newState: Partial<UserMessageState>) => void
getCurrentThreadStagingSelections: () => StagingSelectionItem[]
setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void
// call to edit a message
editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise<void>;
// call to add a message
addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise<void>;
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
cancelStreaming(threadId: string): void;
dismissStreamError(threadId: string): void;
@@ -130,32 +197,107 @@ class ChatThreadService extends Disposable implements IChatThreadService {
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IModelService private readonly _modelService: IModelService,
@IVoidFileService private readonly _voidFileService: IVoidFileService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IToolsService private readonly _toolsService: IToolsService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
) {
super()
const oldVersionNum = this._storageService.get(THREAD_VERSION_KEY, StorageScope.APPLICATION)
const readThreads = this._readAllThreads()
const updatedThreads = this._updatedThreadsToVersion(readThreads, oldVersionNum)
if (updatedThreads !== null) {
this._storeAllThreads(updatedThreads)
}
const allThreads = updatedThreads ?? readThreads
this.state = {
allThreads: this._readAllThreads(),
allThreads: allThreads,
currentThreadId: null as unknown as string, // gets set in startNewThread()
currentStagingSelections: null,
}
// always be in a thread
this.openNewThread()
this._storageService.store(THREAD_VERSION_KEY, LATEST_THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
}
// !!! this is important for properly restoring URIs from storage
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
return JSON.parse(threadsStr, (key, value) => {
if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI
return URI.from(value);
}
return value;
});
}
private _readAllThreads(): ChatThreads {
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
return threads ? JSON.parse(threads) : {}
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION);
if (!threadsStr) {
return {};
}
return this._convertThreadDataFromStorage(threadsStr);
}
private _storeAllThreads(threads: ChatThreads) {
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
const serializedThreads = JSON.stringify(threads);
this._storageService.store(
THREAD_STORAGE_KEY,
serializedThreads,
StorageScope.APPLICATION,
StorageTarget.USER
);
}
// returns if should update
private _updatedThreadsToVersion(oldThreadsObject: any, oldVersion: string | undefined): ChatThreads | null {
if (!oldVersion) {
// unknown, just reset chat?
return null
}
/** v1 -> v2
- threads.state.currentStagingSelections: CodeStagingSelection[] | null;
+ thread[threadIdx].state
+ message.state
+ chatMessage.staging: StagingInfo | null
*/
else if (oldVersion === 'v1') {
const threads = oldThreadsObject as Omit<ChatThreads, 'staging' | 'focusedMessageIdx'>
// update the threads
for (const thread of Object.values(threads)) {
if (!thread.state) {
thread.state = defaultThreadState
}
for (const chatMessage of Object.values(thread.messages)) {
if (chatMessage.role === 'user' && !chatMessage.state) {
chatMessage.state = defaultMessageState
}
}
}
// push the update
return threads
}
else if (oldVersion === 'v2') {
return null
}
// up to date
return null
}
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
this.state = {
@@ -166,6 +308,17 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._onDidChangeCurrentThread.fire()
}
private _getAllSelections() {
const thread = this.getCurrentThread()
return thread.messages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _getSelectionsUpToMessageIdx(messageIdx: number) {
const thread = this.getCurrentThread()
const prevMessages = thread.messages.slice(0, messageIdx)
return prevMessages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
this.streamState[threadId] = {
...this.streamState[threadId],
@@ -177,58 +330,170 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// ---------- streaming ----------
finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
// add assistant's message to chat history, and clear selection
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
this._addMessageToThread(threadId, assistantHistoryElt)
this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null })
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
}
async addUserMessageAndStreamResponse(userMessage: string) {
const threadId = this.getCurrentThread().id
const currSelns = this.state.currentStagingSelections ?? []
const selections = !currSelns ? null : await Promise.all(
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(this._modelService, sel.fileURI) }))
).then(
(files) => files.filter(file => file.content !== null) as CodeSelection[]
)
async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) {
const thread = this.getCurrentThread()
if (thread.messages?.[messageIdx]?.role !== 'user') {
throw new Error("Error: editing a message with role !=='user'")
}
// get prev and curr selections before clearing the message
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx)
const currSelns = thread.messages[messageIdx].selections || []
// clear messages up to the index
const slicedMessages = thread.messages.slice(0, messageIdx)
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
messages: slicedMessages
}
}
}, true)
// re-add the message and stream it
this.addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections: { prevSelns, currSelns } })
}
async addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections }: { userMessage: string, chatMode: ChatMode, chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) {
const thread = this.getCurrentThread()
const threadId = thread.id
// selections in all past chats, then in current chat (can have many duplicates here)
const prevSelns: StagingSelectionItem[] = chatSelections?.prevSelns ?? this._getAllSelections()
const currSelns: StagingSelectionItem[] = chatSelections?.currSelns ?? thread.state.stagingSelections
// add user's message to chat history
const instructions = userMessage
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
this._addMessageToThread(threadId, userHistoryElt)
const userMessageContent = await chat_userMessageContent(instructions, currSelns)
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService)
const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr)
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
this._setStreamState(threadId, { error: undefined })
const llmCancelToken = this._llmMessageService.sendLLMMessage({
logging: { loggingName: 'Chat' },
messages: [
{ role: 'system', content: chat_systemMessage },
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })),
],
onText: ({ newText, fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })
},
onFinalMessage: ({ fullText: content }) => {
this.finishStreaming(threadId, content)
},
onError: (error) => {
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
},
useProviderFor: 'Ctrl+L',
})
if (llmCancelToken === null) return
this._setStreamState(threadId, { streamingToken: llmCancelToken })
const tools: InternalToolInfo[] | undefined = (
chatMode === 'chat' ? undefined
: chatMode === 'agent' ? Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName])
: undefined)
// agent loop
const agentLoop = async () => {
let shouldSendAnotherMessage = true
let nMessagesSent = 0
while (shouldSendAnotherMessage) {
shouldSendAnotherMessage = false
nMessagesSent += 1
let res_: () => void
const awaitable = new Promise<void>((res, rej) => { res_ = res })
// replace last userMessage with userMessageFullContent (which contains all the files too)
const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m)))
const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user')
let messages = messages_
if (lastUserMsgIdx !== -1) { // should never be -1
messages = [
...messages.slice(0, lastUserMsgIdx),
{ role: 'user', content: userMessageFullContent },
...messages.slice(lastUserMsgIdx + 1, Infinity)]
}
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
useProviderFor: 'Ctrl+L',
logging: { loggingName: `Agent` },
messages: [
{ role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)) },
...messages,
],
tools: tools,
onText: ({ fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })
},
onFinalMessage: async ({ fullText, toolCalls }) => {
if ((toolCalls?.length ?? 0) === 0) {
this._finishStreamingTextMessage(threadId, fullText)
}
else {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText })
this._setStreamState(threadId, { messageSoFar: undefined }) // clear streaming message
for (const tool of toolCalls ?? []) {
const toolName = tool.name as ToolName
// 1.
let toolResult: Awaited<ReturnType<ToolFns[ToolName]>>
let toolResultVal: ToolCallReturnType[ToolName]
try {
toolResult = await this._toolsService.toolFns[toolName](tool.params)
toolResultVal = toolResult
} catch (error) {
this._setStreamState(threadId, { error })
shouldSendAnotherMessage = false
break
}
// 2.
let toolResultStr: string
try {
toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
} catch (error) {
this._setStreamState(threadId, { error })
shouldSendAnotherMessage = false
break
}
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResultVal, })
shouldSendAnotherMessage = true
}
}
res_()
},
onError: (error) => {
this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
res_()
},
})
if (llmCancelToken === null) break
this._setStreamState(threadId, { streamingToken: llmCancelToken })
await awaitable
}
}
agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs
}
cancelStreaming(threadId: string) {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '')
this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '')
}
dismissStreamError(threadId: string): void {
@@ -241,12 +506,29 @@ class ChatThreadService extends Disposable implements IChatThreadService {
getCurrentThread(): ChatThreads[string] {
const state = this.state
return state.allThreads[state.currentThreadId];
return state.allThreads[state.currentThreadId]
}
getFocusedMessageIdx() {
const thread = this.getCurrentThread()
// get the focusedMessageIdx
const focusedMessageIdx = thread.state.focusedMessageIdx
if (focusedMessageIdx === undefined) return;
// check that the message is actually being edited
const focusedMessage = thread.messages[focusedMessageIdx]
if (focusedMessage.role !== 'user') return;
if (!focusedMessage.state) return;
return focusedMessageIdx
}
isFocusingMessage() {
return this.getFocusedMessageIdx() !== undefined
}
switchToThread(threadId: string) {
// console.log('threadId', threadId)
// console.log('messages', this.state.allThreads[threadId].messages)
this._setState({ currentThreadId: threadId }, true)
}
@@ -291,12 +573,99 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
// sets the currently selected message (must be undefined if no message is selected)
setFocusedMessageIdx(messageIdx: number | undefined) {
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
state: {
...thread.state,
focusedMessageIdx: messageIdx,
}
}
}
}, true)
}
// set message.state
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
messages: thread.messages.map((m, i) =>
i === messageIdx && m.role === 'user' ? {
...m,
state: {
...m.state,
...state
},
} : m
)
}
}
}, true)
}
// set thread.state
private _setCurrentThreadState(state: Partial<ThreadType['state']>): void {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
state: {
...thread.state,
...state
}
}
}
}, true)
}
getCurrentThreadStagingSelections = () => {
return this.getCurrentThread().state.stagingSelections
}
setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => {
this._setCurrentThreadState({ stagingSelections })
}
// gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected)
getCurrentMessageState(messageIdx: number): UserMessageState {
const currMessage = this.getCurrentThread()?.messages?.[messageIdx]
if (!currMessage || currMessage.role !== 'user') return defaultMessageState
return currMessage.state
}
setCurrentMessageState(messageIdx: number, newState: Partial<UserMessageState>) {
const currMessage = this.getCurrentThread()?.messages?.[messageIdx]
if (!currMessage || currMessage.role !== 'user') return
this._setCurrentMessageState(newState, messageIdx)
}
}
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
@@ -0,0 +1,354 @@
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Position } from '../../../../editor/common/core/position.js';
import { DocumentSymbol, SymbolKind } from '../../../../editor/common/languages.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Range, IRange } from '../../../../editor/common/core/range.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { URI } from '../../../../base/common/uri.js';
// make sure snippet logic works
// change logic for `visited` to intervals
// atomically set new snippets at end
// throttle cache setting
interface IVisitedInterval {
uri: string;
startLine: number;
endLine: number;
}
export interface IContextGatheringService {
readonly _serviceBrand: undefined;
updateCache(model: ITextModel, pos: Position): Promise<void>;
getCachedSnippets(): string[];
}
export const IContextGatheringService = createDecorator<IContextGatheringService>('contextGatheringService');
class ContextGatheringService extends Disposable implements IContextGatheringService {
_serviceBrand: undefined;
private readonly _NUM_LINES = 3;
private readonly _MAX_SNIPPET_LINES = 7; // Reasonable size for context
// Cache holds the most recent list of snippets.
private _cache: string[] = [];
private _snippetIntervals: IVisitedInterval[] = [];
constructor(
@ILanguageFeaturesService private readonly _langFeaturesService: ILanguageFeaturesService,
@IModelService private readonly _modelService: IModelService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService
) {
super();
this._modelService.getModels().forEach(model => this._subscribeToModel(model));
this._register(this._modelService.onModelAdded(model => this._subscribeToModel(model)));
}
private _subscribeToModel(model: ITextModel): void {
console.log("Subscribing to model:", model.uri.toString());
this._register(model.onDidChangeContent(() => {
const editor = this._codeEditorService.getFocusedCodeEditor();
if (editor && editor.getModel() === model) {
const pos = editor.getPosition();
console.log("updateCache called at position:", pos);
if (pos) {
this.updateCache(model, pos);
}
}
}));
}
public async updateCache(model: ITextModel, pos: Position): Promise<void> {
const snippets = new Set<string>();
this._snippetIntervals = []; // Reset intervals for new cache update
await this._gatherNearbySnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals);
await this._gatherParentSnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals);
// Convert to array and filter overlapping snippets
this._cache = Array.from(snippets);
console.log("Cache updated:", this._cache);
}
public getCachedSnippets(): string[] {
return this._cache;
}
// Basic snippet extraction.
private _getSnippetForRange(model: ITextModel, range: IRange, numLines: number): string {
const startLine = Math.max(range.startLineNumber - numLines, 1);
const endLine = Math.min(range.endLineNumber + numLines, model.getLineCount());
// Enforce maximum snippet size
const totalLines = endLine - startLine + 1;
const adjustedStartLine = totalLines > this._MAX_SNIPPET_LINES
? endLine - this._MAX_SNIPPET_LINES + 1
: startLine;
const snippetRange = new Range(adjustedStartLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._cleanSnippet(model.getValueInRange(snippetRange));
}
private _cleanSnippet(snippet: string): string {
return snippet
.split('\n')
// Remove empty lines and lines with only comments
.filter(line => {
const trimmed = line.trim();
return trimmed && !/^\/\/+$/.test(trimmed);
})
// Rejoin with newlines
.join('\n')
// Remove excess whitespace
.trim();
}
private _normalizeSnippet(snippet: string): string {
return snippet
// Remove multiple newlines
.replace(/\n{2,}/g, '\n')
// Remove trailing whitespace
.trim();
}
private _addSnippetIfNotOverlapping(
model: ITextModel,
range: IRange,
snippets: Set<string>,
visited: IVisitedInterval[]
): void {
const startLine = range.startLineNumber;
const endLine = range.endLineNumber;
const uri = model.uri.toString();
if (!this._isRangeVisited(uri, startLine, endLine, visited)) {
visited.push({ uri, startLine, endLine });
const snippet = this._normalizeSnippet(this._getSnippetForRange(model, range, this._NUM_LINES));
if (snippet.length > 0) {
snippets.add(snippet);
}
}
}
private async _gatherNearbySnippets(
model: ITextModel,
pos: Position,
numLines: number,
depth: number,
snippets: Set<string>,
visited: IVisitedInterval[]
): Promise<void> {
if (depth <= 0) return;
const startLine = Math.max(pos.lineNumber - numLines, 1);
const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount());
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
this._addSnippetIfNotOverlapping(model, range, snippets, visited);
const symbols = await this._getSymbolsNearPosition(model, pos, numLines);
for (const sym of symbols) {
const defs = await this._getDefinitionSymbols(model, sym);
for (const def of defs) {
const defModel = this._modelService.getModel(def.uri);
if (defModel) {
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited);
await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited);
}
}
}
}
private async _gatherParentSnippets(
model: ITextModel,
pos: Position,
numLines: number,
depth: number,
snippets: Set<string>,
visited: IVisitedInterval[]
): Promise<void> {
if (depth <= 0) return;
const container = await this._findContainerFunction(model, pos);
if (!container) return;
const containerRange = container.kind === SymbolKind.Method ? container.selectionRange : container.range;
this._addSnippetIfNotOverlapping(model, containerRange, snippets, visited);
const symbols = await this._getSymbolsNearRange(model, containerRange, numLines);
for (const sym of symbols) {
const defs = await this._getDefinitionSymbols(model, sym);
for (const def of defs) {
const defModel = this._modelService.getModel(def.uri);
if (defModel) {
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited);
await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited);
}
}
}
const containerPos = new Position(containerRange.startLineNumber, containerRange.startColumn);
await this._gatherParentSnippets(model, containerPos, numLines, depth - 1, snippets, visited);
}
private _isRangeVisited(uri: string, startLine: number, endLine: number, visited: IVisitedInterval[]): boolean {
return visited.some(interval =>
interval.uri === uri &&
!(endLine < interval.startLine || startLine > interval.endLine)
);
}
private async _getSymbolsNearPosition(model: ITextModel, pos: Position, numLines: number): Promise<DocumentSymbol[]> {
const startLine = Math.max(pos.lineNumber - numLines, 1);
const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount());
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._getSymbolsInRange(model, range);
}
private async _getSymbolsNearRange(model: ITextModel, range: IRange, numLines: number): Promise<DocumentSymbol[]> {
const centerLine = Math.floor((range.startLineNumber + range.endLineNumber) / 2);
const startLine = Math.max(centerLine - numLines, 1);
const endLine = Math.min(centerLine + numLines, model.getLineCount());
const searchRange = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._getSymbolsInRange(model, searchRange);
}
private async _getSymbolsInRange(model: ITextModel, range: IRange): Promise<DocumentSymbol[]> {
const symbols: DocumentSymbol[] = [];
const providers = this._langFeaturesService.documentSymbolProvider.ordered(model);
for (const provider of providers) {
try {
const result = await provider.provideDocumentSymbols(model, CancellationToken.None);
if (result) {
const flat = this._flattenSymbols(result);
const intersecting = flat.filter(sym => this._rangesIntersect(sym.range, range));
symbols.push(...intersecting);
}
} catch (e) {
console.warn("Symbol provider error:", e);
}
}
// Also check reference providers.
const refProviders = this._langFeaturesService.referenceProvider.ordered(model);
for (let line = range.startLineNumber; line <= range.endLineNumber; line++) {
const content = model.getLineContent(line);
const words = content.match(/[a-zA-Z_]\w*/g) || [];
for (const word of words) {
const startColumn = content.indexOf(word) + 1;
const pos = new Position(line, startColumn);
if (!this._positionInRange(pos, range)) continue;
for (const provider of refProviders) {
try {
const refs = await provider.provideReferences(model, pos, { includeDeclaration: true }, CancellationToken.None);
if (refs) {
const filtered = refs.filter(ref => this._rangesIntersect(ref.range, range));
for (const ref of filtered) {
symbols.push({
name: word,
detail: '',
kind: SymbolKind.Variable,
range: ref.range,
selectionRange: ref.range,
children: [],
tags: []
});
}
}
} catch (e) {
console.warn("Reference provider error:", e);
}
}
}
}
return symbols;
}
private _flattenSymbols(symbols: DocumentSymbol[]): DocumentSymbol[] {
const flat: DocumentSymbol[] = [];
for (const sym of symbols) {
flat.push(sym);
if (sym.children && sym.children.length > 0) {
flat.push(...this._flattenSymbols(sym.children));
}
}
return flat;
}
private _rangesIntersect(a: IRange, b: IRange): boolean {
return !(
a.endLineNumber < b.startLineNumber ||
a.startLineNumber > b.endLineNumber ||
(a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) ||
(a.startLineNumber === b.endLineNumber && a.endColumn > b.endColumn)
);
}
private _positionInRange(pos: Position, range: IRange): boolean {
return pos.lineNumber >= range.startLineNumber &&
pos.lineNumber <= range.endLineNumber &&
(pos.lineNumber !== range.startLineNumber || pos.column >= range.startColumn) &&
(pos.lineNumber !== range.endLineNumber || pos.column <= range.endColumn);
}
// Get definition symbols for a given symbol.
private async _getDefinitionSymbols(model: ITextModel, symbol: DocumentSymbol): Promise<(DocumentSymbol & { uri: URI })[]> {
const pos = new Position(symbol.range.startLineNumber, symbol.range.startColumn);
const providers = this._langFeaturesService.definitionProvider.ordered(model);
const defs: (DocumentSymbol & { uri: URI })[] = [];
for (const provider of providers) {
try {
const res = await provider.provideDefinition(model, pos, CancellationToken.None);
if (res) {
const links = Array.isArray(res) ? res : [res];
defs.push(...links.map(link => ({
name: symbol.name,
detail: symbol.detail,
kind: symbol.kind,
range: link.range,
selectionRange: link.range,
children: [],
tags: symbol.tags || [],
uri: link.uri // Now keeping it as URI instead of converting to string
})));
}
} catch (e) {
console.warn("Definition provider error:", e);
}
}
return defs;
}
private async _findContainerFunction(model: ITextModel, pos: Position): Promise<DocumentSymbol | null> {
const searchRange = new Range(
Math.max(pos.lineNumber - 1, 1), 1,
Math.min(pos.lineNumber + 1, model.getLineCount()),
model.getLineMaxColumn(pos.lineNumber)
);
const symbols = await this._getSymbolsInRange(model, searchRange);
const funcs = symbols.filter(s =>
(s.kind === SymbolKind.Function || s.kind === SymbolKind.Method) &&
this._positionInRange(pos, s.range)
);
if (!funcs.length) return null;
return funcs.reduce((innermost, current) => {
if (!innermost) return current;
const moreInner =
(current.range.startLineNumber > innermost.range.startLineNumber ||
(current.range.startLineNumber === innermost.range.startLineNumber &&
current.range.startColumn > innermost.range.startColumn)) &&
(current.range.endLineNumber < innermost.range.endLineNumber ||
(current.range.endLineNumber === innermost.range.endLineNumber &&
current.range.endColumn < innermost.range.endColumn));
return moreInner ? current : innermost;
}, null as DocumentSymbol | null);
}
}
registerSingleton(IContextGatheringService, ContextGatheringService, InstantiationType.Eager);
@@ -91,10 +91,7 @@ export class ConsistentItemService extends Disposable {
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// when an editor is deleted, remove its items
this._register(this._editorService.onCodeEditorRemove(editor => {
removeItemsFromEditor(editor)
}))
this._register(this._editorService.onCodeEditorRemove(editor => { removeItemsFromEditor(editor) }))
}
@@ -1,3 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// eg "bash" -> "shell"
export const nameToVscodeLanguage: { [key: string]: string } = {
@@ -3,6 +3,8 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
class SurroundingsRemover {
readonly originalS: string
i: number
@@ -57,12 +59,12 @@ class SurroundingsRemover {
// return offset === suffix.length
// }
removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => {
removeFromStartUntilFullMatch = (until: string, alsoRemoveUntilStr: boolean) => {
const index = this.originalS.indexOf(until, this.i)
if (index === -1) {
this.i = this.j + 1
return false
return null
}
// console.log('index', index, until.length)
@@ -76,47 +78,55 @@ class SurroundingsRemover {
removeCodeBlock = () => {
// Match either:
// 1. ```language\n<code>\n```\n?
// 2. ```<code>\n```\n?
const pm = this
const foundCodeBlock = pm.removePrefix('```')
if (!foundCodeBlock) return false
pm.removeFromStartUntil('\n', true) // language
pm.removeFromStartUntilFullMatch('\n', true) // language
const j = pm.j
let foundCodeBlockEnd = pm.removeSuffix('```')
if (pm.j === j) foundCodeBlockEnd = pm.removeSuffix('```\n') // if no change, try again with \n after ```
const foundCodeBlockEnd = pm.removeSuffix('```')
if (!foundCodeBlockEnd) return false
pm.removeSuffix('\n')
pm.removeSuffix('\n') // remove the newline before ```
return true
}
actualRecentlyAdded = (recentlyAddedTextLen: number) => {
deltaInfo = (recentlyAddedTextLen: number) => {
// aaaaaatextaaaaaa{recentlyAdded}
// i ^ j
// ^ i j len
// |
// recentyAddedIdx
const recentlyAddedIdx = this.j - recentlyAddedTextLen + 1
return this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
const recentlyAddedIdx = this.originalS.length - recentlyAddedTextLen
const actualDelta = this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
const ignoredSuffix = this.originalS.substring(Math.max(this.j + 1, recentlyAddedIdx), Infinity)
return [actualDelta, ignoredSuffix] as const
}
}
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => {
// Match either:
// 1. ```language\n<code>```
// 2. ```<code>```
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string, string] => {
const pm = new SurroundingsRemover(text)
pm.removeCodeBlock()
const s = pm.value()
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
return [s, actual]
return [s, delta, ignoredSuffix]
}
@@ -124,7 +134,7 @@ export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: s
// Ollama has its own FIM, we should not use this if we use that
export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string] => {
export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string, string] => {
/* ------------- summary of the regex -------------
[optional ` | `` | ```]
@@ -146,24 +156,87 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
pm.removeSuffix(`</${midTag}>`)
}
const s = pm.value()
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
return [s, actual]
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
// const regex = new RegExp(
// `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
// ''
// );
// const match = text.match(regex);
// if (match) {
// const [_, languageName, codeBetweenMidTags] = match;
// return [languageName, codeBetweenMidTags] as const
// } else {
// return [undefined, extractCodeFromRegular(text)] as const
// }
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
return [s, delta, ignoredSuffix]
}
export type ExtractedSearchReplaceBlock = {
state: 'writingOriginal' | 'writingFinal' | 'done',
orig: string,
final: string,
}
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
// for each prefix
for (let i = anyPrefix.length; i >= 0; i--) {
const prefix = anyPrefix.slice(0, i)
if (str.endsWith(prefix)) return prefix
}
return null
}
// guarantees if you keep adding text, array length will strictly grow and state will progress without going back
export const extractSearchReplaceBlocks = (str: string) => {
const ORIGINAL_ = ORIGINAL + `\n`
const DIVIDER_ = '\n' + DIVIDER + `\n`
// logic for FINAL_ is slightly more complicated - should be '\n' + FINAL, but that ignores if the final output is empty
const blocks: ExtractedSearchReplaceBlock[] = []
let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way)
while (true) {
let origStart = str.indexOf(ORIGINAL_, i)
if (origStart === -1) { return blocks }
origStart += ORIGINAL_.length
i = origStart
// wrote <<<< ORIGINAL
let dividerStart = str.indexOf(DIVIDER_, i)
if (dividerStart === -1) { // if didnt find DIVIDER_, either writing originalStr or DIVIDER_ right now
const isWritingDIVIDER = endsWithAnyPrefixOf(str, DIVIDER_)
blocks.push({
orig: str.substring(origStart, str.length - (isWritingDIVIDER?.length ?? 0)),
final: '',
state: 'writingOriginal'
})
return blocks
}
const origStrDone = str.substring(origStart, dividerStart)
dividerStart += DIVIDER_.length
i = dividerStart
// wrote =====
const finalStartA = str.indexOf(FINAL, i)
const finalStartB = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive
const FINAL_ = finalStartB !== -1 ? '\n' + FINAL : FINAL
let finalStart = finalStartB !== -1 ? finalStartB : finalStartA
if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now
const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_)
blocks.push({
orig: origStrDone,
final: str.substring(dividerStart, str.length - (isWritingFINAL?.length ?? 0)),
state: 'writingFinal'
})
return blocks
}
const finalStrDone = str.substring(dividerStart, finalStart)
finalStart += FINAL_.length
i = finalStart
// wrote >>>>> FINAL
blocks.push({
orig: origStrDone,
final: finalStrDone,
state: 'done'
})
}
}
@@ -1,10 +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 { URI } from '../../../../../base/common/uri'
import { EndOfLinePreference } from '../../../../../editor/common/model'
import { IModelService } from '../../../../../editor/common/services/model.js'
import { IFileService } from '../../../../../platform/files/common/files'
// attempts to read URI of currently opened model, then of raw file
export const VSReadFile = async (uri: URI, modelService: IModelService, fileService: IFileService) => {
const modelResult = await _VSReadModel(modelService, uri)
if (modelResult) return modelResult
const fileResult = await _VSReadFileRaw(fileService, uri)
if (fileResult) return fileResult
return ''
// read files from VSCode
export const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
const model = modelService.getModel(uri)
if (!model) return null
return model.getValue(EndOfLinePreference.LF)
}
// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.)
const _VSReadModel = async (modelService: IModelService, uri: URI): Promise<string | null> => {
// attempt to read saved model (doesn't work if application was reloaded...)
const model = modelService.getModel(uri)
if (model) {
return model.getValue(EndOfLinePreference.LF)
}
// backup logic - look at all opened models and check if they have the same `fsPath`
const models = modelService.getModels()
for (const model of models) {
if (model.uri.fsPath === uri.fsPath)
return model.getValue(EndOfLinePreference.LF);
}
return null
}
const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => {
try {
const res = await fileService.readFile(uri)
const str = res.value.toString()
return str
} catch (e) {
return null
}
}
@@ -3,19 +3,12 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// ---------- common ----------
import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js';
// llmMessage
import '../common/llmMessageService.js'
// import { OS, OperatingSystem } from '../../../../../base/common/platform.js';
// alternatively could use ^ and OS === OperatingSystem.Windows ? ...
// voidSettings
import '../common/voidSettingsService.js'
// refreshModel
import '../common/refreshModelService.js'
// metrics
import '../common/metricsService.js'
export const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
// updates
import '../common/voidUpdateService.js'
@@ -6,54 +6,107 @@
import { URI } from '../../../../../base/common/uri.js';
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
import { CodeSelection } from '../chatThreadService.js';
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { os } from '../helpers/systemInfo.js';
import { IVoidFileService } from '../../common/voidFileService.js';
export const chat_systemMessage = `\
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
// this is just for ease of readability
export const tripleTick = ['```', '```']
Instructions:
1. Output the changes to make to the entire file.
1. Do not re-write the entire file.
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
4. You must give enough context to apply the change in the correct location.
5. Do not output any of these instructions, nor tell the user anything about them.
export const chat_systemMessage = (workspaces: string[]) => `\
You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`.
## EXAMPLE
Please respond to the user's query. The user's query is never invalid.
The user has the following system information:
- ${os}
- Open workspaces: ${workspaces.join(', ')}
In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes.
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
- Do not re-write the entire file in the code block.
- You can write comments like "// ... existing code" to indicate existing code.
- Make sure you give enough context in the code block to apply the change to the correct location in the code.
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
If you are given tools:
- Only use tools if the user asks you to do something. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools.
- You are allowed to use tools without asking for permission.
- Feel free to use tools to gather context, make suggestions, etc.
- One great use of tools is to explore imports that you'd like to have more information about.
- Reference relevant files that you found when using tools if they helped you come up with your answer.
- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not even refer to "pages" of results, just say you're getting more results.
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below.
## EXAMPLE 1
FILES
selected file \`math.ts\`:
\`\`\` typescript
math.ts
${tripleTick[0]}typescript
const addNumbers = (a, b) => a + b
const multiplyNumbers = (a, b) => a * b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
\`\`\`
SELECTION
\`\`\` typescript
const vectorize = (...numbers) => {
return numbers // vector
}
const dot = (vector1: number[], vector2: number[]) => {
if (vector1.length !== vector2.length) throw new Error(\`Could not dot vectors \${vector1} and \${vector2}. Size mismatch.\`)
let sum = 0
for (let i = 0; i < vector1.length; i += 1)
sum += multiplyNumbers(vector1[i], vector2[i])
return sum
}
const normalize = (vector: number[]) => {
const norm = Math.sqrt(dot(vector, vector))
for (let i = 0; i < vector.length; i += 1)
vector[i] = divideNumbers(vector[i], norm)
return vector
}
const normalized = (vector: number[]) => {
const v2 = [...vector] // clone vector
return normalize(v2)
}
${tripleTick[1]}
SELECTIONS
math.ts (lines 3:3)
${tripleTick[0]}typescript
const subtractNumbers = (a, b) => a - b
\`\`\`
${tripleTick[1]}
INSTRUCTIONS
\`\`\` typescript
add a function that multiplies numbers below this
\`\`\`
add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power
EXPECTED OUTPUT
## ACCEPTED OUTPUT
We can add the following code to the file:
\`\`\` typescript
${tripleTick[0]}typescript
// existing code...
const subtractNumbers = (a, b) => a - b;
const multiplyNumbers = (a, b) => a * b;
const subtractNumbers = (a, b) => a - b
const exponentiateNumbers = (a, b) => Math.pow(a, b)
const divideNumbers = (a, b) => a / b
// existing code...
\`\`\`
## EXAMPLE
const raiseAll = (vector: number[], power: number) => {
for (let i = 0; i < vector.length; i += 1)
vector[i] = exponentiateNumbers(vector[i], power)
return vector
}
${tripleTick[1]}
## EXAMPLE 2
FILES
selected file \`fib.ts\`:
\`\`\` typescript
fib.ts
${tripleTick[0]}typescript
const dfs = (root) => {
if (!root) return;
@@ -65,21 +118,20 @@ const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
\`\`\`
${tripleTick[1]}
SELECTION
\`\`\` typescript
SELECTIONS
fib.ts (lines 10:10)
${tripleTick[0]}typescript
return fib(n - 1) + fib(n - 2)
\`\`\`
${tripleTick[1]}
INSTRUCTIONS
\`\`\` typescript
memoize results
\`\`\`
EXPECTED OUTPUT
## ACCEPTED OUTPUT
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
\`\`\` typescript
${tripleTick[0]}typescript
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
@@ -87,7 +139,7 @@ const fib = (n, memo = {}) => {
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
\`\`\`
${tripleTick[1]}
Explanation:
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
@@ -97,191 +149,269 @@ Store Result: After computing fib(n), the result is stored in memo for future re
`
const stringifySelections = (selections: CodeSelection[]) => {
return selections.map(({ fileURI, content, selectionStr }) =>
`\
File: ${fileURI.fsPath}
\`\`\` ${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${content // this was the enite file which is foolish
}
\`\`\`${selectionStr === null ? '' : `
Selection: ${selectionStr}`}
`).join('\n')
type FileSelnLocal = { fileURI: URI, content: string }
const stringifyFileSelection = ({ fileURI, content }: FileSelnLocal) => {
return `\
${fileURI.fsPath}
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${content}
${tripleTick[1]}
`
}
const stringifyCodeSelection = ({ fileURI, selectionStr, range }: CodeSelection) => {
return `\
${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber})
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${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[], voidFileService: IVoidFileService) => {
if (fileSelections.length === 0) return null
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
const content = await voidFileService.readFile(sel.fileURI) ?? failToReadStr
return { ...sel, content }
}))
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
}
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') || null
}
const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => {
if (!currSelns) return ''
return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n')
}
export const chat_prompt = (instructions: string, selections: CodeSelection[] | null) => {
let str = '';
if (selections && selections.length > 0) {
str += stringifySelections(selections);
str += `Please edit the selected code following these instructions:\n`
}
str += `${instructions}`;
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, voidFileService: IVoidFileService) => {
// 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)
}
}
}
const filesStr = await stringifyFileSelections(fileSelections, voidFileService)
const selnsStr = stringifyCodeSelections(codeSelections)
if (filesStr || selnsStr) return `\
ALL FILE CONTENTS
${filesStr}
${selnsStr}`
export const ctrlLStream_systemMessage = `
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
return null
}
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | null) => {
if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
else return userMessage
}
export const rewriteCode_systemMessage = `\
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
Directions:
1. Continue exactly where the new file \`new_file\` left off.
1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
# Example 1:
ORIGINAL_FILE
\`Sidebar.tsx\`:
\`\`\` typescript
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\` typescript
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
NEW_FILE
\`\`\` typescript
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
\`\`\`
COMPLETION
\`\`\` typescript
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<div
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</div>
</li>
))}
</ul>
<div className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</div>
</div>
);
};
export default Sidebar;\`\`\`
3. ONLY output the full new file. Do not add any other explanations or text.
`
export const ctrlLStream_prompt = ({ originalCode, userMessage, uri }: { originalCode: string, userMessage: string, uri: URI }) => {
export const rewriteCode_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
return `\
ORIGINAL_CODE
\`\`\` ${language}
ORIGINAL_FILE
${tripleTick[0]}${language}
${originalCode}
\`\`\`
${tripleTick[1]}
DIFF
\`\`\`
${userMessage}
\`\`\`
CHANGE
${tripleTick[0]}
${applyStr}
${tripleTick[1]}
INSTRUCTIONS
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation.
`
}
export const ctrlKStream_systemMessage = `\
export const aiRegex_computeReplacementsForFile_systemMessage = `\
You are a "search and replace" coding assistant.
You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE.
The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for.
The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace.
The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes.
## Instructions
1. If you do not want to make any changes, you should respond with the word "no".
2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make.
For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name.
- Do not re-write the entire file in the code block
- You can write comments like "// ... existing code" to indicate existing code
- Make sure you give enough context in the code block to apply the changes to the correct location in the code`
export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => {
// we may want to do this in batches
const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null }
const file = await stringifyFileSelections([fileSelection], voidFileService)
return `\
## FILE
${file}
## SEARCH_CLAUSE
Here is what the user is searching for:
${searchClause}
## REPLACE_CLAUSE
Here is what the user wants to replace it with:
${replaceClause}
## INSTRUCTIONS
Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.`
}
// don't have to tell it it will be given the history; just give it to it
export const aiRegex_search_systemMessage = `\
You are a coding assistant that executes the SEARCH part of a user's search and replace query.
You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context.
Output
- Regex query
- Files to Include (optional)
- Files to Exclude? (optional)
`
export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
export const ORIGINAL = `<<<<<<< ORIGINAL`
export const DIVIDER = `=======`
export const FINAL = `>>>>>>> UPDATED`
export const searchReplace_systemMessage = `\
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
${tripleTick[0]}
${ORIGINAL}
// ... original code goes here
${DIVIDER}
// ... final code goes here
${FINAL}
${tripleTick[1]}
You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make.
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
Directions:
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
2. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file.
3. The original code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
4. The original code in each SEARCH/REPLACE block must be disjoint from all other blocks.
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
- Make sure you add all necessary imports.
- Make sure the "final" code is complete and will not result in syntax/lint errors.
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
## EXAMPLE 1
ORIGINAL_FILE
${tripleTick[0]}
let w = 5
let x = 6
let y = 7
let z = 8
${tripleTick[1]}
CHANGE
Make x equal to 6.5, not 6.
${tripleTick[0]}
// ... existing code
let x = 6.5
// ... existing code
${tripleTick[1]}
## ACCEPTED OUTPUT
${tripleTick[0]}
${ORIGINAL}
let x = 6
${DIVIDER}
let x = 6.5
${FINAL}
${tripleTick[1]}
`
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
ORIGINAL_FILE
${originalCode}
CHANGE
${applyStr}
INSTRUCTIONS
Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation.
`
export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
const fullFileLines = fullFileStr.split('\n')
@@ -330,22 +460,37 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
}
export type FimTagsType = {
export type QuickEditFimTagsType = {
preTag: string,
sufTag: string,
midTag: string
}
export const defaultFimTags: FimTagsType = {
preTag: 'BEFORE',
sufTag: 'AFTER',
export const defaultQuickEditFimTags: QuickEditFimTagsType = {
preTag: 'ABOVE',
sufTag: 'BELOW',
midTag: 'SELECTION',
}
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, isOllamaFIM, language }:
{
selection: string, prefix: string, suffix: string, userMessage: string, fimTags: FimTagsType, language: string,
isOllamaFIM: false, // we require this be false for clarity
}) => {
// this should probably be longer
export const ctrlKStream_systemMessage = ({ quickEditFIMTags: { preTag, midTag, sufTag } }: { quickEditFIMTags: QuickEditFimTagsType }) => {
return `\
You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags.
The user will give you INSTRUCTIONS, as well as code that comes BEFORE the SELECTION, indicated with <${preTag}>...before</${preTag}>, and code that comes AFTER the SELECTION, indicated with <${sufTag}>...after</${sufTag}>.
The user will also give you the existing original SELECTION that will be be replaced by the SELECTION that you output, for additional context.
Instructions:
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_code</${midTag}>. Do NOT output any text or explanations before or after this.
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
`
}
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
}) => {
const { preTag, sufTag, midTag } = fimTags
// prompt the model artifically on how to do FIM
@@ -353,300 +498,20 @@ export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fim
// const sufTag = 'AFTER'
// const midTag = 'SELECTION'
return `\
The user is selecting this code as their SELECTION:
\`\`\` ${language}
CURRENT SELECTION
${tripleTick[0]}${language}
<${midTag}>${selection}</${midTag}>
\`\`\`
${tripleTick[1]}
The user wants to apply the following INSTRUCTIONS to the SELECTION:
${userMessage}
INSTRUCTIONS
${instructions}
Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection.
Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before</${preTag}>.
Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after</${sufTag}>.
Instructions:
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection</${midTag}>. Do NOT output any text or explanations before or after this.
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
Given the code:
<${preTag}>${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection</${midTag}>\`\`\`):`
Return only the completion block of code (of the form ${tripleTick[0]}${language}
<${midTag}>...new code</${midTag}>
${tripleTick[1]}).`
};
// export const searchDiffChunkInstructions = `
// You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
// Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
// # Example 1:
// FILES
// selected file \`Sidebar.tsx\`:
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// <li key={index}>
// <button
// className={styles.sidebarButton}
// onClick={() => onItemSelect?.(item.label)}
// >
// {item.label}
// </button>
// </li>
// ))}
// </ul>
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
// Extra Action
// </button>
// </div>
// );
// };
// export default Sidebar;
// \`\`\`
// DIFF
// \`\`\`
// @@ ... @@
// -<div className={styles.sidebar}>
// -<ul>
// - {items.map((item, index) => (
// - <li key={index}>
// - <button
// - className={styles.sidebarButton}
// - onClick={() => onItemSelect?.(item.label)}
// - >
// - {item.label}
// - </button>
// - </li>
// - ))}
// -</ul>
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
// - Extra Action
// -</button>
// -</div>
// +<div className={styles.sidebar}>
// +<ul>
// + {items.map((item, index) => (
// + <li key={index}>
// + <div
// + className={styles.sidebarButton}
// + onClick={() => onItemSelect?.(item.label)}
// + >
// + {item.label}
// + </div>
// + </li>
// + ))}
// +</ul>
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
// + Extra Action
// +</div>
// +</div>
// \`\`\`
// SELECTION
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// \`\`\`
// RESULT
// The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
// OUTPUT
// \`true\`
// `
// export const generateDiffInstructions = `
// You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
// Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
// All changes made to files must be outputted in unified diff format.
// Unified diff format instructions:
// 1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
// 2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
// 3. Make diffs more than a few lines.
// 4. Make high-level diffs rather than many one-line diffs.
// Here's an example of unified diff format:
// \`\`\`
// @@ ... @@
// -def factorial(n):
// - if n == 0:
// - return 1
// - else:
// - return n * factorial(n-1)
// +def factorial(number):
// + if number == 0:
// + return 1
// + else:
// + return number * factorial(number-1)
// \`\`\`
// Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
// \`\`\`
// @@ ... @@ # This is less preferred because edits are close together and should be grouped:
// -def factorial(n):
// +def factorial(number):
// - if n == 0:
// + if number == 0:
// return 1
// else:
// - return n * factorial(n-1)
// + return number * factorial(number-1)
// \`\`\`
// # Example 1:
// FILES
// selected file \`test.ts\`:
// \`\`\`
// x = 1
// {{selection}}
// z = 3
// \`\`\`
// SELECTION
// \`\`\`const y = 2\`\`\`
// INSTRUCTIONS
// \`\`\`y = 3\`\`\`
// EXPECTED RESULT
// We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
// \`\`\`
// @@ ... @@
// -x = 1
// -
// -y = 2
// +x = 1
// +
// +y = 3
// \`\`\`
// # Example 2:
// FILES
// selected file \`Sidebar.tsx\`:
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// <li key={index}>
// {{selection}}
// className={styles.sidebarButton}
// onClick={() => onItemSelect?.(item.label)}
// >
// {item.label}
// </button>
// </li>
// ))}
// </ul>
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
// Extra Action
// </button>
// </div>
// );
// };
// export default Sidebar;
// \`\`\`
// SELECTION
// \`\`\` <button\`\`\`
// INSTRUCTIONS
// \`\`\`make all the buttons like this into divs\`\`\`
// EXPECTED OUTPUT
// We should change all the buttons like the one selected into a div component. Here is the change:
// \`\`\`
// @@ ... @@
// -<div className={styles.sidebar}>
// -<ul>
// - {items.map((item, index) => (
// - <li key={index}>
// - <button
// - className={styles.sidebarButton}
// - onClick={() => onItemSelect?.(item.label)}
// - >
// - {item.label}
// - </button>
// - </li>
// - ))}
// -</ul>
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
// - Extra Action
// -</button>
// -</div>
// +<div className={styles.sidebar}>
// +<ul>
// + {items.map((item, index) => (
// + <li key={index}>
// + <div
// + className={styles.sidebarButton}
// + onClick={() => onItemSelect?.(item.label)}
// + >
// + {item.label}
// + </div>
// + </li>
// + ))}
// +</ul>
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
// + Extra Action
// +</div>
// +</div>
// \`\`\`
// `;
@@ -7,17 +7,16 @@ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IInlineDiffsService } from './inlineDiffsService.js';
import { IEditCodeService } from './editCodeService.js';
import { roundRangeToLines } from './sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
import { localize2 } from '../../../../nls.js';
import { IMetricsService } from '../common/metricsService.js';
export type QuickEditPropsType = {
diffareaid: number,
initStreamingDiffZoneId: number | null,
textAreaRef: (ref: HTMLTextAreaElement | null) => void;
onChangeHeight: (height: number) => void;
onChangeText: (text: string) => void;
@@ -63,7 +62,7 @@ registerAction2(class extends Action2 {
const { startLineNumber: startLine, endLineNumber: endLine } = selection
const inlineDiffsService = accessor.get(IInlineDiffsService)
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
const editCodeService = accessor.get(IEditCodeService)
editCodeService.addCtrlKZone({ startLine, endLine, editor })
}
});
@@ -80,6 +80,22 @@ const args = process.argv.slice(2);
const isWatch = args.includes('--watch') || args.includes('-w');
if (isWatch) {
// this just builds it if it doesn't exist instead of waiting for the watcher to trigger
// Check if src2/ exists; if not, do an initial scope-tailwind build
if (!fs.existsSync('src2')) {
try {
console.log('🔨 Running initial scope-tailwind build to create src2 folder...');
execSync(
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"',
{ stdio: 'inherit' }
);
console.log('✅ src2/ created successfully.');
} catch (err) {
console.error('❌ Error running initial scope-tailwind build:', err);
process.exit(1);
}
}
// Watch mode
const scopeTailwindWatcher = spawn('npx', [
'nodemon',
@@ -0,0 +1,156 @@
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js'
import { useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'
enum CopyButtonText {
Idle = 'Copy',
Copied = 'Copied!',
Error = 'Could not copy',
}
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CopyButton = ({ codeStr }: { codeStr: string }) => {
const accessor = useAccessor()
const metricsService = accessor.get('IMetricsService')
const clipboardService = accessor.get('IClipboardService')
const [copyButtonText, setCopyButtonText] = useState(CopyButtonText.Idle)
useEffect(() => {
if (copyButtonText === CopyButtonText.Idle) return
setTimeout(() => {
setCopyButtonText(CopyButtonText.Idle)
}, COPY_FEEDBACK_TIMEOUT)
}, [copyButtonText])
const onCopy = useCallback(() => {
clipboardService.writeText(codeStr)
.then(() => { setCopyButtonText(CopyButtonText.Copied) })
.catch(() => { setCopyButtonText(CopyButtonText.Error) })
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
}, [metricsService, clipboardService, codeStr, setCopyButtonText])
const isSingleLine = !codeStr.includes('\n')
return <button
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onCopy}
>
{copyButtonText}
</button>
}
// state persisted for duration of react only
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef)
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const [_, rerender] = useState(0)
const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri])
// listen for stream updates
useURIStreamState(
useCallback((uri, newStreamState) => {
const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath
if (shouldUpdate) return
rerender(c => c + 1)
}, [applyBoxId, editCodeService, applyingUri])
)
const onSubmit = useCallback(() => {
if (isDisabled) return
if (streamState() === 'streaming') return
const newApplyingUri = editCodeService.startApplying({
from: 'ClickApply',
type: 'searchReplace',
applyStr: codeStr,
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
rerender(c => c + 1)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [isDisabled, streamState, editCodeService, codeStr, applyBoxId, metricsService])
const onInterrupt = useCallback(() => {
if (streamState() !== 'streaming') return
const uri = applyingUri()
if (!uri) return
editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {})
}, [streamState, applyingUri, editCodeService, metricsService])
const isSingleLine = !codeStr.includes('\n')
const applyButton = <button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onSubmit}
>
Apply
</button>
const stopButton = <button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onInterrupt}
>
Stop
</button>
const acceptRejectButtons = <>
<button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={() => {
const uri = applyingUri()
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}}
>
Accept
</button>
<button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={() => {
const uri = applyingUri()
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}}
>
Reject
</button>
</>
console.log('streamStateRef.current', streamState())
const currStreamState = streamState()
return <>
{currStreamState !== 'streaming' && <CopyButton codeStr={codeStr} />}
{currStreamState === 'idle' && !isDisabled && applyButton}
{currStreamState === 'streaming' && stopButton}
{currStreamState === 'acceptRejectAll' && acceptRejectButtons}
</>
}
@@ -15,8 +15,7 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov
<>
<div className="relative group w-full overflow-hidden">
{buttonsOnHover === null ? null : (
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''
}`}>
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''}`}>
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
{buttonsOnHover}
</div>
@@ -3,72 +3,21 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { JSX, useCallback, useEffect, useState } from 'react'
import React, { JSX } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
import { useAccessor } from '../util/services.js'
import { ChatMessageLocation, } from '../../../aiRegexService.js'
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
enum CopyButtonState {
Copy = 'Copy',
Copied = 'Copied!',
Error = 'Could not copy',
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
return `${threadId}-${messageIdx}-${tokenIdx}`
}
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ text }: { text: string }) => {
const accessor = useAccessor()
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
const inlineDiffService = accessor.get('IInlineDiffsService')
const clipboardService = accessor.get('IClipboardService')
const metricsService = accessor.get('IMetricsService')
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
}, COPY_FEEDBACK_TIMEOUT)
}
}, [copyButtonState])
const onCopy = useCallback(() => {
clipboardService.writeText(text)
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
metricsService.capture('Copy Code', { length: text.length }) // capture the length only
}, [text, clipboardService])
const onApply = useCallback(() => {
inlineDiffService.startApplying({
featureName: 'Ctrl+L',
userMessage: text,
})
metricsService.capture('Apply Code', { length: text.length }) // capture the length only
}, [inlineDiffService])
const isSingleLine = !text.includes('\n')
return <>
<button
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onCopy}
>
{copyButtonState}
</button>
<button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onApply}
>
Apply
</button>
</>
}
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
return <code className={`
@@ -84,7 +33,8 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c
</code>
}
const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => {
const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
@@ -94,11 +44,20 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
}
if (t.type === "code") {
return <BlockCode
const applyBoxId = chatMessageLocation ? getApplyBoxId({
threadId: chatMessageLocation.threadId,
messageIdx: chatMessageLocation.messageIdx,
tokenIdx: tokenIdx,
}) : null
return <div className='my-4'>
<BlockCode
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} // use vscode to detect language
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
/>
</div>
}
if (t.type === "heading") {
@@ -172,32 +131,50 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
)}
<span className="ml-1">
<ChatMarkdownRender string={item.text} nested={true} />
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
</span>
</li>
))}
</ListTag>
)
// attempt at indentation
// return (
// <ListTag
// start={t.start ? t.start : undefined}
// className={`pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
// >
// {t.items.map((item, index) => (
// <li key={index} className={`${noSpace ? '' : 'mb-2'} ml-4`}>
// {item.task && (
// <input type="checkbox" className='mr-2 form-checkbox' checked={item.checked} readOnly />
// )}
// <span className-='inline-block pr-2'>
// <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
// </span>
// </li>
// ))}
// </ListTag>
// )
}
if (t.type === "paragraph") {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index} token={token} />
<RenderToken key={index} token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} /> // assign a unique tokenId to nested components
))}
</>
if (nested)
return contents
return <p className={`${noSpace ? '' : 'my-4'} leading`}>{contents}</p>
if (nested) return contents
return <p className={`${noSpace ? '' : 'my-4'}`}>
{contents}
</p>
}
if (t.type === "html") {
return (
<pre className={`bg-4oid-bg-2 p-4 rounded-lg ${noSpace ? '' : 'my-4'} font-mono text-sm`}>
{`<html>`}
<p className={`${noSpace ? '' : 'my-4'}`}>
{t.raw}
{`</html>`}
</pre>
</p>
)
}
@@ -266,12 +243,12 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
)
}
export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => {
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} />
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} />
))}
</>
)
@@ -4,18 +4,18 @@
*--------------------------------------------------------------------------------------*/
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js';
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.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 = ({
diffareaid,
initStreamingDiffZoneId,
onChangeHeight,
onChangeText: onChangeText_,
textAreaRef: textAreaRef_,
@@ -23,7 +23,7 @@ export const QuickEditChat = ({
}: QuickEditPropsType) => {
const accessor = useAccessor()
const inlineDiffsService = accessor.get('IInlineDiffsService')
const editCodeService = accessor.get('IEditCodeService')
const sizerRef = useRef<HTMLDivElement | null>(null)
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
@@ -42,148 +42,87 @@ export const QuickEditChat = ({
}, [onChangeHeight]);
const settingsState = useSettingsState()
// state of current message
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
const isDisabled = instructionsAreEmpty
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState)
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
const isStreaming = currStreamingDiffZoneRef.current !== null
const onSubmit = useCallback((e: FormEvent) => {
const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCtrlKZoneStreaming({ diffareaid }))
useCtrlKZoneStreamingState(useCallback((diffareaid2, isStreaming) => {
if (diffareaid !== diffareaid2) return
setIsStreamingRef(isStreaming)
}, [diffareaid, setIsStreamingRef]))
const onSubmit = useCallback(() => {
if (isDisabled) return
if (currStreamingDiffZoneRef.current !== null) return
if (isStreamingRef.current) return
textAreaFnsRef.current?.disable()
const instructions = textAreaRef.current?.value ?? ''
const id = inlineDiffsService.startApplying({
featureName: 'Ctrl+K',
diffareaid: diffareaid,
userMessage: instructions,
editCodeService.startApplying({
from: 'QuickEdit',
type: 'rewrite',
diffareaid,
})
setCurrentlyStreamingDiffZone(id ?? null)
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid])
}, [isStreamingRef, isDisabled, editCodeService, diffareaid])
const onInterrupt = useCallback(() => {
if (currStreamingDiffZoneRef.current === null) return
inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current)
setCurrentlyStreamingDiffZone(null)
if (!isStreamingRef.current) return
editCodeService.interruptCtrlKStreaming({ diffareaid })
textAreaFnsRef.current?.enable()
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService])
}, [isStreamingRef, editCodeService])
const onX = useCallback(() => {
onInterrupt()
inlineDiffsService.removeCtrlKZone({ diffareaid })
}, [inlineDiffsService, diffareaid])
editCodeService.removeCtrlKZone({ diffareaid })
}, [editCodeService, diffareaid])
useScrollbarStyles(sizerRef)
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
return <div ref={sizerRef} style={{ maxWidth: 500 }} className={`py-2 w-full`}>
<form
// copied from SidebarChat.tsx
className={`
flex flex-col gap-2 p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
`}
onClick={(e) => {
textAreaRef.current?.focus()
}}
const chatAreaRef = useRef<HTMLDivElement | null>(null)
return <div ref={sizerRef} style={{ maxWidth: 450 }} className={`py-2 w-full`}>
<VoidChatArea
divRef={chatAreaRef}
onSubmit={onSubmit}
onAbort={onInterrupt}
onClose={onX}
isStreaming={isStreamingRef.current}
isDisabled={isDisabled}
featureName="Ctrl+K"
className="py-2 w-full"
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
{/* // this div is used to position the input box properly */}
<div
className={`w-full z-[999] relative`}
>
<div className='flex flex-row items-center justify-between items-end gap-1'>
{/* input */}
<div // copied from SidebarChat.tsx
className={`w-full`}
>
{/* text input */}
<VoidInputBox2
className='px-1'
initValue={initText}
ref={useCallback((r: HTMLTextAreaElement | null) => {
textAreaRef.current = r
textAreaRef_(r)
// if presses the esc key, X
r?.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
}, [textAreaRef_, onX])}
fnsRef={textAreaFnsRef}
placeholder={`Enter instructions...`}
// ${keybindingString} to select.
onChangeText={useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
onChangeText_(newStr)
}, [onChangeText_])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit(e)
return
}
}}
multiline={true}
/>
</div>
{/* X button */}
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
<IconX
size={16}
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
onClick={onX}
/>
</div>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+K' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
/>
:
// submit button (up arrow)
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
<VoidInputBox2
className='px-1'
initValue={initText}
ref={useCallback((r: HTMLTextAreaElement | null) => {
textAreaRef.current = r
textAreaRef_(r)
r?.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
}, [textAreaRef_, onX])}
fnsRef={textAreaFnsRef}
placeholder="Enter instructions..."
onChangeText={useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
onChangeText_(newStr)
}, [onChangeText_])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
return
}
</div>
</div>
</form>
}}
multiline={true}
/>
</VoidChatArea>
</div>
@@ -5,11 +5,12 @@
import React, { useEffect, useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js';
import { useSettingsState } from '../util/services.js';
export const ErrorDisplay = ({
message:message_,
message: message_,
fullError,
onDismiss,
showDismiss,
@@ -22,9 +23,9 @@ export const ErrorDisplay = ({
const [isExpanded, setIsExpanded] = useState(false);
const details = errorDetails(fullError)
const isExpandable = !!details
const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_
const message = message_ + ''
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
@@ -45,7 +46,7 @@ export const ErrorDisplay = ({
</div>
<div className='flex gap-2'>
{details && (
{isExpandable && (
<button className='text-red-600 hover:text-red-800 p-1 rounded'
onClick={() => setIsExpanded(!isExpanded)}
>
File diff suppressed because it is too large Load Diff
@@ -68,13 +68,14 @@ export const SidebarThreadSelector = () => {
let firstMsg = null;
// let secondMsg = null;
const firstMsgIdx = pastThread.messages.findIndex(
(msg) => msg.role !== 'system' && !!msg.displayContent
const firstUserMsgIdx = pastThread.messages.findIndex(
(msg) => msg.role !== 'system' && msg.role !== 'tool' && !!msg.displayContent
);
if (firstMsgIdx !== -1) {
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? '';
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
@@ -15,6 +15,7 @@ import { useAccessor } from './services.js';
import { ITextModel } from '../../../../../../../editor/common/model.js';
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
// type guard
@@ -57,9 +58,11 @@ type InputBox2Props = {
className?: string;
onChangeText?: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
onChangeHeight?: (newHeight: number) => void;
}
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onChangeText }, ref) {
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
// mirrors whatever is in ref
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
@@ -113,6 +116,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
adjustHeight()
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
onFocus={onFocus}
onBlur={onBlur}
disabled={!isEnabled}
className={`w-full resize-none max-h-[500px] overflow-y-auto text-void-fg-1 placeholder:text-void-fg-3 ${className}`}
@@ -146,12 +152,13 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
})
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, isPasswordField, multiline }: {
onChangeText: (value: string) => void;
styles?: Partial<IInputBoxStyles>,
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
inputBoxRef?: { current: InputBox | null };
placeholder: string;
isPasswordField?: boolean;
multiline: boolean;
}) => {
@@ -176,6 +183,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
},
placeholder,
tooltip: '',
type: isPasswordField ? 'password' : undefined,
flexibleHeight: multiline,
flexibleMaxHeight: 500,
flexibleWidth: false,
@@ -296,148 +304,147 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
}
export const VoidCustomSelectBox = <T extends any>({
export const VoidCustomDropdownBox = <T extends any>({
options,
selectedOption: selectedOption_,
selectedOption,
onChangeOption,
getOptionDropdownName,
getOptionDropdownDetail,
getOptionDisplayName,
getOptionsEqual,
className,
arrowTouchesText = true,
matchInputWidth = false,
isMenuPositionFixed = true,
gap = 0,
}: {
options: T[];
selectedOption?: T;
selectedOption: T | undefined;
onChangeOption: (newValue: T) => void;
getOptionDropdownName: (option: T) => string;
getOptionDropdownDetail?: (option: T) => string;
getOptionDisplayName: (option: T) => string;
getOptionsEqual: (a: T, b: T) => boolean;
className?: string;
arrowTouchesText?: boolean;
matchInputWidth?: boolean;
isMenuPositionFixed?: boolean;
gap?: number;
}) => {
const [isOpen, setIsOpen] = useState(false);
const [readyToShow, setReadyToShow] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
const containerRef = useRef<HTMLDivElement | null>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const measureRef = useRef<HTMLDivElement | null>(null);
const measureRef = useRef<HTMLDivElement>(null);
// Replace manual positioning with floating-ui
const {
x,
y,
strategy,
refs,
middlewareData,
update
} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: 'bottom-start',
// if the selected option is null, use the 0th option as the selected, and set the option to options[0]
middleware: [
offset(gap),
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',
});
// if the selected option is null, set the selection to the 0th option
useEffect(() => {
if (!options[0]) return
if (!selectedOption_) {
onChangeOption(options[0]);
}
}, [selectedOption_, options])
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
const updatePosition = useCallback(() => {
if (!buttonRef.current || !containerRef.current || !measureRef.current) return;
const buttonRect = buttonRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const containerWidth = containerRef.current.offsetWidth;
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - buttonRect.bottom;
const spaceNeeded = options.length * 28;
const showAbove = spaceBelow < spaceNeeded && buttonRect.top > spaceBelow;
// Calculate the menu width
let menuWidth = matchInputWidth ? containerWidth : buttonRect.width;
// If not matchInputWidth, calculate content width from measurement div
if (!matchInputWidth) {
const contentWidth = measureRef.current.offsetWidth;
menuWidth = Math.max(buttonRect.width, contentWidth);
}
if (isMenuPositionFixed) {
// Fixed positioning (relative to viewport)
setPosition({
top: showAbove
? buttonRect.top - spaceNeeded
: buttonRect.bottom + gap,
left: buttonRect.left,
width: menuWidth,
});
} else {
// Absolute positioning (relative to parent container)
setPosition({
top: showAbove
? -(spaceNeeded + gap)
: buttonRect.height + gap,
left: 0,
width: menuWidth,
});
}
setReadyToShow(true);
}, [gap, matchInputWidth, options.length, isMenuPositionFixed]);
if (options.length === 0) return
if (selectedOption) return
onChangeOption(options[0])
}, [selectedOption, onChangeOption, options])
// Handle clicks outside
useEffect(() => {
if (isOpen) {
setReadyToShow(false);
updatePosition();
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
if (!isOpen) return;
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
} else {
setReadyToShow(false);
}
}, [isOpen, updatePosition]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
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)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isOpen]);
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, refs.floating, refs.reference]);
if (!selectedOption)
return null
return (
<div
ref={containerRef}
className={`inline-block relative ${className}`}
>
<div className={`inline-block relative ${className}`}>
{/* Hidden measurement div */}
<div
ref={measureRef}
className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col"
aria-hidden="true"
>
{options.map((option) => (
<div key={getOptionDropdownName(option)} className="flex items-center whitespace-nowrap">
<div className="w-4" />
<span className="px-2">{getOptionDropdownName(option)}</span>
</div>
))}
{options.map((option) => {
const optionName = getOptionDropdownName(option);
const optionDetail = getOptionDropdownDetail?.(option) || '';
return (
<div key={optionName + optionDetail} className="flex items-center whitespace-nowrap">
<div className="w-4" />
<span className="flex justify-between w-full">
<span>{optionName}</span>
<span>{optionDetail}</span>
<span>______</span>
</span>
</div>
)
})}
</div>
{/* Select Button */}
<button
type='button'
ref={buttonRef}
ref={refs.setReference}
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
onClick={() => {
setIsOpen(!isOpen);
}}
onClick={() => setIsOpen(!isOpen)}
>
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
{getOptionDisplayName(selectedOption)}
@@ -458,18 +465,26 @@ export const VoidCustomSelectBox = <T extends any>({
</button>
{/* Dropdown Menu */}
{isOpen && readyToShow && (
{isOpen && (
<div
className={`${isMenuPositionFixed ? 'fixed' : 'absolute'} z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg`}
ref={refs.setFloating}
className="z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg"
style={{
top: position.top,
left: position.left,
width: position.width,
position: strategy,
top: y ?? 0,
left: x ?? 0,
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) || '';
return (
<div
@@ -497,7 +512,10 @@ export const VoidCustomSelectBox = <T extends any>({
</svg>
)}
</div>
<span>{optionName}</span>
<span className="flex justify-between w-full">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
</span>
</div>
);
})}
@@ -670,7 +688,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
verticalScrollbarSize: 0,
horizontal: 'auto',
horizontalScrollbarSize: 8,
ignoreHorizontalScrollbarInContentHeight: true,
// ignoreHorizontalScrollbarInContentHeight: true,
},
},
@@ -710,7 +728,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
const model = modelOfEditorId[id] ?? modelService.createModel(
initValueRef.current, {
initValueRef.current + '\n', {
languageId: languageRef.current ? languageRef.current : 'typescript',
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
})
@@ -3,32 +3,29 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useCallback } from 'react'
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { VoidUriState } from '../../../voidUriStateService.js';
import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
import { IExplorerService } from '../../../../../../../workbench/contrib/files/browser/files.js'
import { IModelService } from '../../../../../../../editor/common/services/model.js';
import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js';
import { IContextViewService, IContextMenuService } from '../../../../../../../platform/contextview/browser/contextView.js';
import { IFileService } from '../../../../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js';
import { IVoidUriStateService } from '../../../voidUriStateService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
@@ -46,7 +43,8 @@ import { IKeybindingService } from '../../../../../../../platform/keybinding/com
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
import { URI } from '../../../../../../../base/common/uri.js'
@@ -79,6 +77,11 @@ const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: Refresh
let colorThemeState: ColorScheme
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set()
const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set()
// must call this before you can use any of the hooks below
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
let wasCalled = false
@@ -103,10 +106,10 @@ export const _registerServices = (accessor: ServicesAccessor) => {
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
themeService: accessor.get(IThemeService),
inlineDiffsService: accessor.get(IInlineDiffsService),
editCodeService: accessor.get(IEditCodeService),
}
const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService } = stateServices
uriState = uriStateService.state
disposables.push(
@@ -162,7 +165,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
refreshModelService.onDidChangeState((providerName) => {
refreshModelState = refreshModelService.state
refreshModelStateListeners.forEach(l => l(refreshModelState))
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState))
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState)) // no state
})
)
@@ -174,6 +177,21 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
// no state
disposables.push(
editCodeService.onDidChangeCtrlKZoneStreaming(({ diffareaid }) => {
const isStreaming = editCodeService.isCtrlKZoneStreaming({ diffareaid })
ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming))
})
)
disposables.push(
editCodeService.onDidChangeURIStreamState(({ uri }) => {
const isStreaming = editCodeService.getURIStreamState({ uri })
uriStreamingStateListeners.forEach(l => l(uri, isStreaming))
})
)
return disposables
}
@@ -192,7 +210,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
ILLMMessageService: accessor.get(ILLMMessageService),
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IInlineDiffsService: accessor.get(IInlineDiffsService),
IEditCodeService: accessor.get(IEditCodeService),
IVoidUriStateService: accessor.get(IVoidUriStateService),
IQuickEditStateService: accessor.get(IQuickEditStateService),
ISidebarStateService: accessor.get(ISidebarStateService),
@@ -209,6 +227,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
IKeybindingService: accessor.get(IKeybindingService),
IExplorerService: accessor.get(IExplorerService),
IEnvironmentService: accessor.get(IEnvironmentService),
IConfigurationService: accessor.get(IConfigurationService),
IPathService: accessor.get(IPathService),
@@ -288,6 +307,17 @@ export const useChatThreadsState = () => {
return () => { chatThreadsStateListeners.delete(ss) }
}, [ss])
return s
// allow user to set state natively in react
// const ss: React.Dispatch<React.SetStateAction<ThreadsState>> = (action)=>{
// _ss(action)
// if (typeof action === 'function') {
// const newState = action(chatThreadsState)
// chatThreadsState = newState
// } else {
// chatThreadsState = action
// }
// }
// return [s, ss] as const
}
@@ -325,7 +355,21 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv
useEffect(() => {
refreshModelProviderListeners.add(listener)
return () => { refreshModelProviderListeners.delete(listener) }
}, [listener])
}, [listener, refreshModelProviderListeners])
}
export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boolean) => void) => {
useEffect(() => {
ctrlKZoneStreamingStateListeners.add(listener)
return () => { ctrlKZoneStreamingStateListeners.delete(listener) }
}, [listener, ctrlKZoneStreamingStateListeners])
}
export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => {
useEffect(() => {
uriStreamingStateListeners.add(listener)
return () => { uriStreamingStateListeners.delete(listener) }
}, [listener, uriStreamingStateListeners])
}
@@ -342,3 +386,4 @@ export const useIsDark = () => {
return isDark
}
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
useEffect(() => {
if (!containerRef.current) return;
@@ -12,90 +11,118 @@ export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivE
'[class*="overflow-y-auto"]'
].join(',');
// 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))
];
// 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) || [])
];
// Apply styles and listeners to each scroll element
scrollElements.forEach(element => {
// Add the scrollable class directly to the overflow element
element.classList.add('void-scrollable-element');
let fadeTimeout: NodeJS.Timeout | null = null;
let fadeInterval: NodeJS.Timeout | null = null;
const fadeIn = () => {
if (fadeInterval) clearInterval(fadeInterval);
let step = 0;
fadeInterval = setInterval(() => {
if (step <= 10) {
element.classList.remove(`show-scrollbar-${step - 1}`);
element.classList.add(`show-scrollbar-${step}`);
step++;
} else {
clearInterval(fadeInterval!);
}
}, 10);
};
const fadeOut = () => {
if (fadeInterval) clearInterval(fadeInterval);
let step = 10;
fadeInterval = setInterval(() => {
if (step >= 0) {
element.classList.remove(`show-scrollbar-${step + 1}`);
element.classList.add(`show-scrollbar-${step}`);
step--;
} else {
clearInterval(fadeInterval!);
}
}, 60);
};
const onMouseEnter = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
fadeIn();
};
const onMouseLeave = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
fadeTimeout = setTimeout(() => {
fadeOut();
}, 10);
};
element.addEventListener('mouseenter', onMouseEnter);
element.addEventListener('mouseleave', onMouseLeave);
// Store cleanup function
const cleanup = () => {
element.removeEventListener('mouseenter', onMouseEnter);
element.removeEventListener('mouseleave', onMouseLeave);
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
element.classList.remove('void-scrollable-element');
// Remove any remaining show-scrollbar classes
for (let i = 0; i <= 10; i++) {
element.classList.remove(`show-scrollbar-${i}`);
}
};
// Store the cleanup function on the element for later use
(element as any).__scrollbarCleanup = cleanup;
});
return () => {
// Clean up all scroll elements
// Apply basic styling to all elements
scrollElements.forEach(element => {
if ((element as any).__scrollbarCleanup) {
(element as any).__scrollbarCleanup();
element.classList.add('void-scrollable-element');
});
// 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;
const fadeIn = () => {
if (fadeInterval) clearInterval(fadeInterval);
let step = 0;
fadeInterval = setInterval(() => {
if (step <= 10) {
element.classList.remove(`show-scrollbar-${step - 1}`);
element.classList.add(`show-scrollbar-${step}`);
step++;
} else {
clearInterval(fadeInterval!);
}
}, 10);
};
const fadeOut = () => {
if (fadeInterval) clearInterval(fadeInterval);
let step = 10;
fadeInterval = setInterval(() => {
if (step >= 0) {
element.classList.remove(`show-scrollbar-${step + 1}`);
element.classList.add(`show-scrollbar-${step}`);
step--;
} else {
clearInterval(fadeInterval!);
}
}, 60);
};
const onMouseEnter = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
fadeIn();
};
const onMouseLeave = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
fadeTimeout = setTimeout(() => {
fadeOut();
}, 10);
};
element.addEventListener('mouseenter', onMouseEnter);
element.addEventListener('mouseleave', onMouseLeave);
// Store cleanup function
const cleanup = () => {
element.removeEventListener('mouseenter', onMouseEnter);
element.removeEventListener('mouseleave', onMouseLeave);
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
element.classList.remove('void-scrollable-element');
// Remove any remaining show-scrollbar classes
for (let i = 0; i <= 10; i++) {
element.classList.remove(`show-scrollbar-${i}`);
}
};
// Store the cleanup function on the element for later use
(element as any).__scrollbarCleanup = cleanup;
}
});
};
// Initialize for the first time
initializeScrollbarStyles();
// Set up mutation observer to do the same
const observer = new MutationObserver(() => {
initializeScrollbarStyles();
});
// Start observing the container for child changes
observer.observe(containerRef.current, {
childList: true,
subtree: true
});
return () => {
observer.disconnect();
// Your existing cleanup code...
if (containerRef.current) {
const scrollElements = [
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
];
scrollElements.forEach(element => {
if ((element as any).__scrollbarCleanup) {
(element as any).__scrollbarCleanup();
}
});
}
};
}, [containerRef]);
};
@@ -4,15 +4,14 @@
*--------------------------------------------------------------------------------------*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { FeatureName, featureNames, isFeatureNameDisabled, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { WarningBox } from './WarningBox.js'
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
if (m1.length !== m2.length) return false
@@ -27,22 +26,22 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
const voidSettingsService = accessor.get('IVoidSettingsService')
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection))! : options[0]
const onChangeOption = useCallback((newOption: ModelOption) => {
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
}, [voidSettingsService, featureName])
return <VoidCustomSelectBox
return <VoidCustomDropdownBox
options={options}
selectedOption={selectedOption}
onChangeOption={onChangeOption}
getOptionDisplayName={(option) => option.selection.modelName}
getOptionDropdownName={(option) => option.name}
getOptionDropdownName={(option) => option.selection.modelName}
getOptionDropdownDetail={(option) => option.selection.providerName }
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
className={`text-xs text-void-fg-3 px-1`}
className='text-xs text-void-fg-3 px-1'
matchInputWidth={false}
isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true}
/>
}
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
@@ -76,10 +75,13 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
// />
// }
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
const oldOptionsRef = useRef<ModelOption[]>([])
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions
@@ -93,30 +95,6 @@ const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) =
}
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
return <div
className={`
text-void-warning brightness-90 opacity-90
text-xs text-ellipsis
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
flex items-center flex-nowrap
${className}
`}
onClick={onClick}
>
<IconWarning
size={14}
className='mr-1'
/>
<span>{text}</span>
</div>
// return <VoidSelectBox
// options={[{ text: 'Please add a model!', value: null }]}
// onChangeSelection={() => { }}
// />
}
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
@@ -125,10 +103,14 @@ export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
return <>
{settingsState._modelOptions.length === 0 ?
<WarningBox onClick={openSettings} text='Provider required' />
: <MemoizedModelSelectBox featureName={featureName} />
}
</>
const isDisabled = isFeatureNameDisabled(featureName, settingsState)
if (isDisabled)
return <WarningBox onClick={openSettings} text={
isDisabled === 'needToEnableModel' ? 'Enable a model'
: isDisabled === 'addModel' ? 'Add a model'
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
: 'Provider required'
} />
return <MemoizedModelDropdown featureName={featureName} />
}
@@ -5,21 +5,23 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { env } from '../../../../../../../base/common/process.js'
import { WarningBox } from './ModelDropdown.js'
import { ModelDropdown } from './ModelDropdown.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
import { WarningBox } from './WarningBox.js'
import { os } from '../../../helpers/systemInfo.js'
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
return <div className='flex items-center text-void-fg-3 mb-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
return <div className='flex items-center text-void-fg-3 px-3 py-0.5 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
<button className='flex items-center' disabled={disabled} onClick={onClick}>
{icon}
</button>
@@ -79,10 +81,8 @@ const RefreshableModels = () => {
const buttons = refreshableProviderNames.map(providerName => {
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
return <div key={providerName} className='pb-4'>
<RefreshModelButton providerName={providerName} />
</div>
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null
return <RefreshModelButton key={providerName} providerName={providerName} />
})
return <>
@@ -112,7 +112,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
<div className='flex items-center gap-4'>
{/* provider */}
<VoidCustomSelectBox
<VoidCustomDropdownBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setProviderName(pn)}
@@ -199,7 +199,7 @@ export const ModelDump = () => {
for (let providerName of providerNames) {
const providerSettings = settingsState.settingsOfProvider[providerName]
// if (!providerSettings.enabled) continue
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._enabled })))
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings })))
}
// sort by hidden
@@ -223,7 +223,6 @@ export const ModelDump = () => {
<div className={`flex-grow flex items-center gap-4`}>
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span className='w-fit truncate'>{modelName}</span>
{/* <span>{`${modelName} (${providerName})`}</span> */}
</div>
{/* right part is anything that fits */}
<div className='flex items-center gap-4'>
@@ -256,11 +255,10 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
// const { title: providerTitle, } = displayInfoOfProviderName(providerName)
const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName)
const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName)
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidMetricsService = accessor.get('IMetricsService')
let weChangedTextRef = false
@@ -269,6 +267,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
<VoidInputBox
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
placeholder={`${settingTitle} (${placeholder})`}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
@@ -284,30 +283,14 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
weChangedTextRef = true
instance.value = stateVal as string
weChangedTextRef = false
const isEverySettingPresent = Object.keys(defaultProviderSettings[providerName]).every(key => {
return !!settingsAtProvider[key as keyof typeof settingsAtProvider]
})
const shouldEnable = isEverySettingPresent && !settingsAtProvider._enabled // enable if all settings are present and not already enabled
const shouldDisable = !isEverySettingPresent && settingsAtProvider._enabled
if (shouldEnable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
voidMetricsService.capture('Enable Provider', { providerName })
}
if (shouldDisable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
voidMetricsService.capture('Disable Provider', { providerName })
}
}
syncInstance()
const disposable = voidSettingsService.onDidChangeState(syncInstance)
return [disposable]
}, [voidSettingsService, providerName, settingName])}
multiline={false}
isPasswordField={isPasswordField}
/>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
<ChatMarkdownRender noSpace string={subTextMd} />
@@ -318,7 +301,10 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
// const voidSettingsState = useSettingsState()
const voidSettingsState = useSettingsState()
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
@@ -349,6 +335,12 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
{needsModel ?
providerName === 'ollama' ?
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
: <WarningBox text={`Please add a model for ${providerTitle} (Models section).`} />
: null}
</div>
</div >
}
@@ -376,15 +368,16 @@ export const AutoRefreshToggle = () => {
// right now this is just `enabled_autoRefreshModels`
const enabled = voidSettingsState.globalSettings[settingName]
return <SubtleButton
onClick={() => {
voidSettingsService.setGlobalSetting(settingName, !enabled)
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
}}
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
disabled={false}
/>
return <SubtleButton
onClick={() => {
voidSettingsService.setGlobalSetting(settingName, !enabled)
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
}}
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
disabled={false}
/>
}
export const AIInstructionsBox = () => {
@@ -392,9 +385,9 @@ export const AIInstructionsBox = () => {
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
return <VoidInputBox2
className='min-h-[81px] p-3 rounded-sm'
className='min-h-[81px] p-3 rounded-sm'
initValue={voidSettingsState.globalSettings.aiInstructions}
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Respond to all queries in French. `}
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Write new code using Rust if possible. `}
multiline
onChangeText={(newText) => {
voidSettingsService.setGlobalSetting('aiInstructions', newText)
@@ -404,7 +397,17 @@ export const AIInstructionsBox = () => {
export const FeaturesTab = () => {
return <>
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
<AutoRefreshToggle />
<RefreshableModels />
<div className='py-2'/>
<ModelDump />
<AddModelMenuFull />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
@@ -429,13 +432,20 @@ export const FeaturesTab = () => {
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Models</h2>
<h2 className={`text-3xl mb-2 mt-12`}>Feature Options</h2>
<ErrorBoundary>
<AutoRefreshToggle />
<RefreshableModels />
<ModelDump />
<AddModelMenuFull />
{featureNames.map(featureName =>
<div key={featureName}
className='mb-2'
>
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
<ModelDropdown featureName={featureName} />
</div>
)}
</ErrorBoundary>
</>
}
@@ -498,7 +508,7 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): Transfe
throw new Error(`os '${os}' not recognized`)
}
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
let transferTheseFiles: TransferFilesInfo = []
let transferError: string | null = null
@@ -513,7 +523,7 @@ const OneClickSwitchButton = () => {
if (transferTheseFiles.length === 0)
return <>
<WarningBox text={transferError ?? `One-click-switch not available.`} />
<WarningBox text={transferError ?? `One-click switch not available.`} />
</>
@@ -616,7 +626,7 @@ export const Settings = () => {
<div className='max-w-5xl mx-auto'>
<h1 className='text-2xl w-full'>Void Settings</h1>
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
{/* separator */}
<div className='w-full h-[1px] my-4' />
@@ -0,0 +1,26 @@
import { IconWarning } from '../sidebar-tsx/SidebarChat.js';
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
return <div
className={`
text-void-warning brightness-90 opacity-90 w-fit
text-xs text-ellipsis
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
flex items-center flex-nowrap
${className}
`}
onClick={onClick}
>
<IconWarning
size={14}
className='mr-1'
/>
<span>{text}</span>
</div>
// return <VoidSelectBox
// options={[{ text: 'Please add a model!', value: null }]}
// onChangeSelection={() => { }}
// />
}
@@ -28,17 +28,25 @@ module.exports = {
colors: {
"void-bg-1": "var(--vscode-input-background)",
"void-bg-1-alt": "var(--vscode-badge-background)",
"void-bg-2": "var(--vscode-sideBar-background)",
"void-bg-2-alt": "color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%)",
"void-bg-3": "var(--vscode-editor-background)",
"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)",
vscode: {
@@ -0,0 +1,46 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
export interface ISearchReplaceService {
readonly _serviceBrand: undefined;
}
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
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: Omit<ServiceSendLLMMessageParams, 'onText'> & { 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);
@@ -11,13 +11,13 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { CodeStagingSelection, IChatThreadService } from './chatThreadService.js';
import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js';
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_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { IMetricsService } from '../common/metricsService.js';
import { ISidebarStateService } from './sidebarStateService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
@@ -67,6 +67,13 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
}
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 {
@@ -112,7 +119,7 @@ registerAction2(class extends Action2 {
const selectionStr = getContentInRange(model, selectionRange)
const selection: CodeStagingSelection = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
fileURI: model.uri,
selectionStr: null,
@@ -124,26 +131,35 @@ registerAction2(class extends Action2 {
range: selectionRange,
}
// add selection to staging
// update the staging selections
const chatThreadService = accessor.get(IChatThreadService)
const currentStaging = chatThreadService.state.currentStagingSelections
const currentStagingEltIdx = currentStaging?.findIndex(s =>
s.fileURI.fsPath === model.uri.fsPath
&& s.range?.startLineNumber === selection.range?.startLineNumber
&& s.range?.endLineNumber === selection.range?.endLineNumber
)
// if matches with existing selection, overwrite
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
chatThreadService.setStaging([
...currentStaging!.slice(0, currentStagingEltIdx),
const focusedMessageIdx = chatThreadService.getFocusedMessageIdx()
// set the selections to the proper value
let selections: StagingSelectionItem[] = []
let setSelections = (s: StagingSelectionItem[]) => { }
if (focusedMessageIdx === undefined) {
selections = chatThreadService.getCurrentThreadStagingSelections()
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadStagingSelections(s)
} else {
selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
}
// if matches with existing selection, overwrite (since text may change)
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
setSelections([
...selections!.slice(0, matchingStagingEltIdx),
selection,
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
...selections!.slice(matchingStagingEltIdx + 1, Infinity)
])
}
// if no match, add
// if no match, add it
else {
chatThreadService.setStaging([...(currentStaging ?? []), selection])
setSelections([...(selections ?? []), selection])
}
}
@@ -223,7 +239,7 @@ registerAction2(class extends Action2 {
constructor() {
super({
id: 'void.settingsAction',
title: 'Void Settings',
title: `Void's Settings`,
icon: { id: 'settings-gear' },
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
});
@@ -5,7 +5,7 @@
// register inline diffs
import './inlineDiffsService.js'
import './editCodeService.js'
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
import './sidebarActions.js'
@@ -21,6 +21,10 @@ import './chatThreadService.js'
// register Autocomplete
import './autocompleteService.js'
// register Context services
// import './contextGatheringService.js'
// import './contextUserChangesService.js'
// settings pane
import './voidSettingsPane.js'
@@ -29,3 +33,26 @@ import './media/void.css'
// update (frontend part, also see platform/)
import './voidUpdateActions.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
import '../common/llmMessageService.js'
// voidSettings
import '../common/voidSettingsService.js'
// refreshModel
import '../common/refreshModelService.js'
// metrics
import '../common/metricsService.js'
// updates
import '../common/voidUpdateService.js'
// tools
import '../common/toolsService.js'
@@ -8,7 +8,7 @@ import { EditorInput } from '../../../common/editor/editorInput.js';
import * as nls from '../../../../nls.js';
import { EditorExtensions } from '../../../common/editor.js';
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IStorageService } from '../../../../platform/storage/common/storage.js';
@@ -35,7 +35,7 @@ class VoidSettingsInput extends EditorInput {
static readonly ID: string = 'workbench.input.void.settings';
static readonly RESOURCE = URI.from({ // I think this scheme is invalid, it just shuts up TS
scheme: 'void', // Custom scheme for our editor
scheme: 'void', // Custom scheme for our editor (try Schemas.https)
path: 'settings'
})
readonly resource = VoidSettingsInput.RESOURCE;
@@ -49,7 +49,7 @@ class VoidSettingsInput extends EditorInput {
}
override getName(): string {
return nls.localize('voidSettingsInputsName', 'Void Settings');
return nls.localize('voidSettingsInputsName', 'Void\'s Settings');
}
override getIcon() {
@@ -112,7 +112,7 @@ class VoidSettingsPane extends EditorPane {
// register Settings pane
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void Settings Pane")),
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void\'s Settings Pane")),
[new SyncDescriptor(VoidSettingsInput)]
);
@@ -141,18 +141,27 @@ registerAction2(class extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService);
const editorGroupService = accessor.get(IEditorGroupsService);
const instantiationService = accessor.get(IInstantiationService);
// close all instances if found
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
if (openEditors.length > 0) {
await editorService.closeEditors(openEditors);
// if is open, close it
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE); // should only have 0 or 1 elements...
if (openEditors.length !== 0) {
const openEditor = openEditors[0].editor
const isCurrentlyOpen = editorService.activeEditor?.resource?.fsPath === openEditor.resource?.fsPath
if (isCurrentlyOpen)
await editorService.closeEditors(openEditors)
else
await editorGroupService.activeGroup.openEditor(openEditor)
return;
}
// else open it
const input = instantiationService.createInstance(VoidSettingsInput);
await editorService.openEditor(input);
await editorGroupService.activeGroup.openEditor(input);
}
})
@@ -193,7 +202,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '0_command',
command: {
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
title: nls.localize('voidSettings', "Void Settings")
title: nls.localize('voidSettings', "Void\'s Settings")
},
order: 1
});
@@ -9,8 +9,8 @@ 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 { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { IVoidUpdateService } from '../../../../platform/void/common/voidUpdateService.js';
import { IMetricsService } from '../common/metricsService.js';
import { IVoidUpdateService } from '../common/voidUpdateService.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
@@ -53,10 +53,11 @@ registerAction2(class extends Action2 {
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: Error', {}) }
else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update: Yes', {}) }
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update: No', {}) }
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Manual: Error', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update Manual: Yes', { res }) }
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update Manual: No', { res }) }
}
})
@@ -65,23 +66,27 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo
static readonly ID = 'workbench.contrib.void.voidUpdate'
constructor(
@IVoidUpdateService private readonly voidUpdateService: IVoidUpdateService,
@INotificationService private readonly notifService: INotificationService,
@IMetricsService private readonly metricsService: IMetricsService,
@INotificationService private readonly notifService: INotificationService,
) {
super()
// on mount
setTimeout(async () => {
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.message); 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 notifService = this.notifService
const metricsService = this.metricsService
// check once 5 seconds after mount
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Startup: Error', {}) }
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); metricsService.capture('Void Update Startup: Yes', {}) }
else if (!res.hasUpdate) { metricsService.capture('Void Update Startup: No', {}) } // display nothing if up to date
const initId = setTimeout(() => autoCheck(), 5 * 1000)
this._register({ dispose: () => clearTimeout(initId) })
// check every 3 hours
const intervalId = setInterval(() => autoCheck(), 3 * 60 * 60 * 1000)
this._register({ dispose: () => clearInterval(intervalId) })
}, 5 * 1000)
}
}
registerWorkbenchContribution2(VoidUpdateWorkbenchContribution.ID, VoidUpdateWorkbenchContribution, WorkbenchPhase.BlockRestore);
@@ -0,0 +1,192 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* 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 './llmMessageTypes.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
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 { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
export interface ILLMMessageService {
readonly _serviceBrand: undefined;
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
abort: (requestId: string) => void;
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
vLLMList: (params: ServiceModelListParams<VLLMModelResponse>) => void;
}
// open this file side by side with llmMessageChannel
export class LLMMessageService extends Disposable implements ILLMMessageService {
readonly _serviceBrand: undefined;
private readonly channel: IChannel // LLMMessageChannel
// sendLLMMessage
private readonly llmMessageHooks = {
onText: {} as { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) },
onFinalMessage: {} as { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) },
onError: {} as { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) },
}
// list hooks
private readonly listHooks = {
ollama: {
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) },
}
} satisfies {
[providerName: string]: {
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<any>) => void) },
error: { [eventId: string]: ((params: EventModelListOnErrorParams<any>) => void) },
}
}
constructor(
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
// @INotificationService private readonly notificationService: INotificationService,
) {
super()
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
// see llmMessageChannel.ts
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
// .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._onRequestIdDone(e.requestId) }))
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._onRequestIdDone(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
// ollama .list()
this._register((this.channel.listen('onSuccess_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) }))
}
sendLLMMessage(params: ServiceSendLLMMessageParams) {
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { useProviderFor: featureName } = proxyParams
// throw an error if no model/provider selected (this should usually never be reached, the UI should check this first, but might happen in cases like Apply where we haven't built much UI/checks yet, good practice to have check logic on backend)
const isDisabled = isFeatureNameDisabled(featureName, this.voidSettingsService.state)
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
if (isDisabled || modelSelection === null) {
let message: string
if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected')
message = `Please add a provider in Void's Settings.`
else if (isDisabled === 'addModel')
message = `Please add a model.`
else if (isDisabled === 'needToEnableModel')
message = `Please enable a model.`
else if (isDisabled === 'notFilledIn')
message = `Please fill in Void's Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
else
message = `Please add a provider in Void's Settings.`
onError({ message, fullError: null })
return null
}
const { providerName, modelName } = modelSelection
// add state for request id
const requestId = generateUuid();
this.llmMessageHooks.onText[requestId] = onText
this.llmMessageHooks.onFinalMessage[requestId] = onFinalMessage
this.llmMessageHooks.onError[requestId] = onError
const { aiInstructions } = this.voidSettingsService.state.globalSettings
const { settingsOfProvider } = this.voidSettingsService.state
// params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', {
...proxyParams,
aiInstructions,
requestId,
providerName,
modelName,
settingsOfProvider,
} satisfies MainSendLLMMessageParams);
return requestId
}
abort(requestId: string) {
this.channel.call('abort', { requestId } satisfies MainLLMMessageAbortParams);
this._onRequestIdDone(requestId)
}
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
// add state for request id
const requestId_ = generateUuid();
this.listHooks.ollama.success[requestId_] = onSuccess
this.listHooks.ollama.error[requestId_] = onError
this.channel.call('ollamaList', {
...proxyParams,
settingsOfProvider,
providerName: 'ollama',
requestId: requestId_,
} satisfies MainModelListParams<OllamaModelResponse>)
}
vLLMList = (params: ServiceModelListParams<VLLMModelResponse>) => {
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.channel.call('vLLMList', {
...proxyParams,
settingsOfProvider,
providerName: 'vLLM',
requestId: requestId_,
} satisfies MainModelListParams<VLLMModelResponse>)
}
_onRequestIdDone(requestId: string) {
delete this.llmMessageHooks.onText[requestId]
delete this.llmMessageHooks.onFinalMessage[requestId]
delete this.llmMessageHooks.onError[requestId]
delete this.listHooks.ollama.success[requestId]
delete this.listHooks.ollama.error[requestId]
delete this.listHooks.vLLM.success[requestId]
delete this.listHooks.vLLM.error[requestId]
}
}
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
@@ -3,8 +3,9 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { IRange } from '../../../editor/common/core/range'
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { ChatMessage } from '../browser/chatThreadService.js'
import { InternalToolInfo, ToolName } from './toolsService.js'
import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@@ -12,6 +13,7 @@ export const errorDetails = (fullError: Error | null): string | null => {
return null
}
else if (typeof fullError === 'object') {
if (Object.keys(fullError).length === 0) return null
return JSON.stringify(fullError, null, 2)
}
else if (typeof fullError === 'string') {
@@ -20,76 +22,101 @@ export const errorDetails = (fullError: Error | null): string | null => {
return null
}
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string }) => void
export type LLMChatMessage = {
role: 'system' | 'user';
content: string;
} | {
role: 'assistant',
content: string;
} | {
role: 'tool';
content: string; // result
name: string;
params: string;
id: string;
}
export type ToolCallType = {
name: ToolName;
params: string;
id: string;
}
export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }
export type LLMMessage = {
role: 'system' | 'user' | 'assistant';
content: string;
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
if (c.role === 'system' || c.role === 'user') {
return { role: c.role, content: c.content || '(empty message)' }
}
else if (c.role === 'assistant')
return { role: c.role, content: c.content || '(empty message)' }
else if (c.role === 'tool')
return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content || '(empty output)' }
else {
throw 1
}
}
export type ServiceSendLLMFeatureParams = {
useProviderFor: 'Ctrl+K';
range: IRange;
export type LLMFIMMessage = {
prefix: string;
suffix: string;
stopTokens: string[];
}
type SendLLMType = {
messagesType: 'chatMessages';
messages: LLMChatMessage[];
tools?: InternalToolInfo[];
} | {
useProviderFor: 'Ctrl+L';
} | {
useProviderFor: 'Autocomplete';
range: IRange;
}
// params to the true sendLLMMessage function
export type LLMMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
abortRef: AbortRef;
messages: LLMMessage[];
logging: {
loggingName: string,
};
providerName: ProviderName;
modelName: string;
settingsOfProvider: SettingsOfProvider;
messagesType: 'FIMMessage';
messages: LLMFIMMessage;
tools?: undefined;
}
// service types
export type ServiceSendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
logging: { loggingName: string, };
useProviderFor: FeatureName;
} & SendLLMType
// params to the true sendLLMMessage function
export type SendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
logging: { loggingName: string, };
abortRef: AbortRef;
aiInstructions: string;
providerName: ProviderName;
modelName: string;
settingsOfProvider: SettingsOfProvider;
} & SendLLMType
messages: LLMMessage[];
logging: {
loggingName: string,
};
} & ServiceSendLLMFeatureParams
// can't send functions across a proxy, use listeners instead
export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
export type MainSendLLMMessageParams = Omit<SendLLMMessageParams, BlockedMainLLMMessageParams> & { requestId: string } & SendLLMType
export type MainLLMMessageParams = Omit<LLMMMessageParams, BlockedMainLLMMessageParams> & { requestId: string }
export type MainLLMMessageAbortParams = { requestId: string }
export type EventLLMMessageOnTextParams = Parameters<OnText>[0] & { requestId: string }
export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0] & { requestId: string }
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
export type _InternalSendLLMMessageFnType = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
settingsOfProvider: SettingsOfProvider;
providerName: ProviderName;
modelName: string;
_setAborter: (aborter: () => void) => void;
}) => void
// service -> main -> internal -> event (back to main)
// (browser)
@@ -102,13 +129,6 @@ export type _InternalSendLLMMessageFnType = (params: {
// These are from 'ollama' SDK
interface OllamaModelDetails {
parent_model: string;
@@ -129,18 +149,22 @@ export type OllamaModelResponse = {
size_vram: number;
}
export type OpenaiCompatibleModelResponse = {
type OpenaiCompatibleModelResponse = {
id: string;
created: number;
object: 'model';
owned_by: string;
}
export type VLLMModelResponse = OpenaiCompatibleModelResponse
// params to the true list fn
export type ModelListParams<modelResponse> = {
export type ModelListParams<ModelResponse> = {
providerName: ProviderName;
settingsOfProvider: SettingsOfProvider;
onSuccess: (param: { models: modelResponse[] }) => void;
onSuccess: (param: { models: ModelResponse[] }) => void;
onError: (param: { error: string }) => void;
}
@@ -159,4 +183,3 @@ export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListPar
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void
@@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { Action2, registerAction2 } from '../../actions/common/actions.js';
import { localize2 } from '../../../nls.js';
import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js';
import { INotificationService } from '../../notification/common/notification.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
import { localize2 } from '../../../../nls.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
export interface IMetricsService {
readonly _serviceBrand: undefined;
@@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { ILLMMessageService } from './llmMessageService.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
import { OllamaModelResponse, VLLMModelResponse } from './llmMessageTypes.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@@ -44,8 +44,9 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_enabled', 'endpoint'],
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
ollama: ['_didFillInProviderSettings', 'endpoint'],
vLLM: ['_didFillInProviderSettings', 'endpoint'],
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
@@ -95,7 +96,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
for (const providerName of refreshableProviderNames) {
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
// const { '_didFillInProviderSettings': enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.startRefreshingModels(providerName, autoOptions)
// every time providerName.enabled changes, refresh models too, like a useEffect
@@ -140,10 +141,11 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
state: RefreshModelStateOfProvider = {
ollama: { state: 'init', timeoutId: null },
vLLM: { state: 'init', timeoutId: null },
}
// start listening for models (and don't stop until success)
// start listening for models (and don't stop)
startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => {
this._clearProviderTimeout(providerName)
@@ -158,7 +160,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
}
}
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
: providerName === 'vLLM' ? this.llmMessageService.vLLMList
: () => { }
listFn({
@@ -169,13 +171,13 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
providerName,
models.map(model => {
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
else if (providerName === 'vLLM') return (model as VLLMModelResponse).id;
else throw new Error('refreshMode fn: unknown provider', providerName);
}),
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
)
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_didFillInProviderSettings', true)
this._setRefreshState(providerName, 'finished', options)
autoPoll()
@@ -0,0 +1,364 @@
import { CancellationToken } from '../../../../base/common/cancellation.js'
import { URI } from '../../../../base/common/uri.js'
import { IFileService } from '../../../../platform/files/common/files.js'
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js'
import { ISearchService } from '../../../../workbench/services/search/common/search.js'
import { IVoidFileService } from './voidFileService.js'
// tool use for AI
// we do this using Anthropic's style and convert to OpenAI style later
export type InternalToolInfo = {
name: string,
description: string,
params: {
[paramName: string]: { type: string, description: string | undefined } // name -> type
},
required: string[], // required paramNames
}
const paginationHelper = {
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, }
} as const
export const voidTools = {
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
},
required: ['uri'],
},
list_dir: {
name: 'list_dir',
description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param
},
required: ['uri'],
},
pathname_search: {
name: 'pathname_search',
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
},
required: ['query']
},
search: {
name: 'search',
description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
},
required: ['query'],
},
// go_to_definition:
// go_to_usages:
// create_file: {
// name: 'create_file',
// description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`
// params: {
// uri: { type: 'string', description: undefined },
// }
// }
// semantic_search: {
// description: 'Searches files semantically for the given string query.',
// // RAG
// },
} satisfies { [name: string]: InternalToolInfo }
export type ToolName = keyof typeof voidTools
export const toolNames = Object.keys(voidTools) as ToolName[]
const toolNamesSet = new Set<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
export type ToolParamNames<T extends ToolName> = keyof typeof voidTools[T]['params']
export type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T>]: unknown }
export type ToolCallReturnType = {
'read_file': { uri: URI, fileContents: string, hasNextPage: boolean },
'list_dir': { rootURI: URI, children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'pathname_search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean },
'search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean }
'create_file': {}
}
type DirectoryItem = {
uri: URI;
name: string;
isDirectory: boolean;
isSymbolicLink: boolean;
}
export type ToolFns = { [T in ToolName]: (p: string) => Promise<ToolCallReturnType[T]> }
export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType[T]) => string }
// pagination info
const MAX_FILE_CHARS_PAGE = 50_000
const MAX_CHILDREN_URIs_PAGE = 500
const computeDirectoryResult = async (
fileService: IFileService,
rootURI: URI,
pageNumber: number = 1
): Promise<ToolCallReturnType['list_dir']> => {
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
if (!stat.isDirectory) {
return { rootURI, 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: DirectoryItem[] = 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 {
rootURI,
children,
hasNextPage,
hasPrevPage,
itemsRemaining
};
};
const directoryResultToString = (result: ToolCallReturnType['list_dir']): string => {
if (!result.children) {
return `Error: ${result.rootURI} is not a directory`;
}
let output = '';
const entries = result.children;
if (!result.hasPrevPage) {
output += `${result.rootURI}\n`;
}
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const isLast = i === entries.length - 1 && !result.hasNextPage;
const prefix = isLast ? '└── ' : '├── ';
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
}
if (result.hasNextPage) {
output += `└── (${result.itemsRemaining} results remaining...)\n`;
}
return output;
};
const validateJSON = (s: string): { [s: string]: unknown } => {
try {
const o = JSON.parse(s)
return o
}
catch (e) {
throw new Error(`Tool parameter was not a valid JSON: "${s}".`)
}
}
const validateQueryStr = (queryStr: unknown) => {
if (typeof queryStr !== 'string') throw new Error('Error calling tool: provided query must be a string.')
return queryStr
}
// TODO!!!! check to make sure in workspace
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.')
const uri = URI.file(uriStr)
return uri
}
const validatePageNum = (pageNumberUnknown: unknown) => {
const proposedPageNum = Number.parseInt(pageNumberUnknown + '')
const num = Number.isInteger(proposedPageNum) ? proposedPageNum : 1
const pageNumber = num < 1 ? 1 : num
return pageNumber
}
export interface IToolsService {
readonly _serviceBrand: undefined;
toolFns: ToolFns;
toolResultToString: ToolResultToString;
}
export const IToolsService = createDecorator<IToolsService>('ToolsService');
export class ToolsService implements IToolsService {
readonly _serviceBrand: undefined;
public toolFns: ToolFns
public toolResultToString: ToolResultToString
constructor(
@IFileService fileService: IFileService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@ISearchService searchService: ISearchService,
@IInstantiationService instantiationService: IInstantiationService,
@IVoidFileService voidFileService: IVoidFileService,
) {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.toolFns = {
read_file: async (s: string) => {
console.log('read_file')
const o = validateJSON(s)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
const readFileContents = await voidFileService.readFile(uri)
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
console.log('read_file result:', fileContents)
return { uri, fileContents, hasNextPage }
},
list_dir: async (s: string) => {
console.log('list_dir')
const o = validateJSON(s)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
const dirResult = await computeDirectoryResult(fileService, uri, pageNumber)
console.log('list_dir result:', dirResult)
return dirResult
},
pathname_search: async (s: string) => {
console.log('pathname_search')
const o = validateJSON(s)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
const queryStr = validateQueryStr(queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, })
const data = await searchService.fileSearch(query, CancellationToken.None)
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
const uris = data.results
.slice(fromIdx, toIdx + 1) // paginate
.map(({ resource, results }) => resource)
const hasNextPage = (data.results.length - 1) - toIdx >= 1
console.log('pathname_search result:', uris)
return { queryStr, uris, hasNextPage }
},
search: async (s: string) => {
console.log('search')
const o = validateJSON(s)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
const queryStr = validateQueryStr(queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri))
const data = await searchService.textSearch(query, CancellationToken.None)
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
const uris = data.results
.slice(fromIdx, toIdx + 1) // paginate
.map(({ resource, results }) => resource)
const hasNextPage = (data.results.length - 1) - toIdx >= 1
console.log('search result:', uris)
return { queryStr, uris, hasNextPage }
},
}
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
this.toolResultToString = {
read_file: (result) => {
return nextPageStr(result.hasNextPage)
},
list_dir: (result) => {
const dirTreeStr = directoryResultToString(result)
return dirTreeStr + nextPageStr(result.hasNextPage)
},
pathname_search: (result) => {
if (typeof result.uris === 'string') return result.uris
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
search: (result) => {
if (typeof result.uris === 'string') return result.uris
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
}
}
}
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
@@ -0,0 +1,109 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { isWindows } from '../../../../base/common/platform.js';
import { URI } from '../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
// linebreak symbols
export const allLinebreakSymbols = ['\r\n', '\n']
export const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
export interface IVoidFileService {
readonly _serviceBrand: undefined;
readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string>;
readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null;
}
export const IVoidFileService = createDecorator<IVoidFileService>('VoidFileService');
// implemented by calling channel
export class VoidFileService implements IVoidFileService {
readonly _serviceBrand: undefined;
constructor(
@IModelService private readonly modelService: IModelService,
@IFileService private readonly fileService: IFileService,
) {
}
readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string> => {
// attempt to read the model
const modelResult = this.readModel(uri, range);
if (modelResult) return modelResult;
// if no model, read the raw file
const fileResult = await this._readFileRaw(uri, range);
if (fileResult) return fileResult;
return '';
}
_readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null> => {
try { // this throws an error if no file exists (eg it was deleted)
const res = await this.fileService.readFile(uri);
if (range) {
return res.value.toString()
.split(_ln)
.slice(range.startLineNumber - 1, range.endLineNumber)
.join(_ln)
}
return res.value.toString();
} catch (e) {
return null;
}
}
readModel = (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null => {
// read saved model (sometimes null if the user reloads application)
let model = this.modelService.getModel(uri);
// check all opened models for the same `fsPath`
if (!model) {
const models = this.modelService.getModels();
for (const m of models) {
if (m.uri.fsPath === uri.fsPath) {
model = m
break;
}
}
}
// if still not found, return
if (!model) { return null }
// if range, read it
if (range) {
return model.getValueInRange({
startLineNumber: range.startLineNumber,
endLineNumber: range.endLineNumber,
startColumn: 1,
endColumn: Number.MAX_VALUE
}, EndOfLinePreference.LF);
} else {
return model.getValue(EndOfLinePreference.LF)
}
}
}
registerSingleton(IVoidFileService, VoidFileService, InstantiationType.Eager);
@@ -3,15 +3,15 @@
* 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 { deepClone } from '../../../base/common/objects.js';
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IEncryptionService } from '../../../../platform/encryption/common/encryptionService.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IMetricsService } from './metricsService.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.settingsServiceStorage'
@@ -42,8 +42,8 @@ export type VoidSettingsState = {
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
// type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
// type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
export interface IVoidSettingsService {
@@ -51,7 +51,7 @@ export interface IVoidSettingsService {
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
onDidChangeState: Event<EventProp>;
onDidChangeState: Event<void>;
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
@@ -64,26 +64,76 @@ export interface IVoidSettingsService {
}
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
let modelOptions: ModelOption[] = []
const _updatedValidatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
let newSettingsOfProvider = state.settingsOfProvider
// recompute _didFillInProviderSettings
for (const providerName of providerNames) {
const providerConfig = settingsOfProvider[providerName]
if (!providerConfig._enabled) continue // if disabled, don't display model options
for (const { modelName, isHidden } of providerConfig.models) {
if (isHidden) continue
modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } })
const settingsAtProvider = newSettingsOfProvider[providerName]
const didFillInProviderSettings = Object.keys(defaultProviderSettings[providerName]).every(key => !!settingsAtProvider[key as keyof typeof settingsAtProvider])
if (didFillInProviderSettings === settingsAtProvider._didFillInProviderSettings) continue
newSettingsOfProvider = {
...newSettingsOfProvider,
[providerName]: {
...settingsAtProvider,
_didFillInProviderSettings: didFillInProviderSettings,
},
}
}
return modelOptions
// update model options
let newModelOptions: ModelOption[] = []
for (const providerName of providerNames) {
const providerTitle = providerName // displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName
if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue // if disabled, don't display model options
for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) {
if (isHidden) continue
newModelOptions.push({ name: `${modelName} (${providerTitle})`, selection: { providerName, modelName } })
}
}
// now that model options are updated, make sure the selection is valid
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
let newModelSelectionOfFeature = state.modelSelectionOfFeature
for (const featureName of featureNames) {
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
const selnIdx = modelSelectionAtFeature === null ? -1 : newModelOptions.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature))
if (selnIdx !== -1) continue
newModelSelectionOfFeature = {
...newModelSelectionOfFeature,
[featureName]: newModelOptions.length === 0 ? null : newModelOptions[0].selection
}
}
const newState = {
...state,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
_modelOptions: newModelOptions,
} satisfies VoidSettingsState
return newState
}
const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
globalSettings: deepClone(defaultGlobalSettings),
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
_modelOptions: [], // computed later
}
return d
}
@@ -93,8 +143,8 @@ export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidS
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<EventProp>();
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
state: VoidSettingsState;
waitForInitState: Promise<void> // await this if you need a valid state initially
@@ -115,13 +165,57 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this.waitForInitState = new Promise((res, rej) => resolver = res)
// read and update the actual state immediately
this._readState().then(s => {
this.state = s
this._readState().then(readS => {
// the stored data structure might be outdated, so we need to update it here (can do a more general solution later when we need to)
const newSettingsOfProvider = {
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
...{ deepseek: defaultSettingsOfProvider.deepseek },
// A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS)
...{ mistral: defaultSettingsOfProvider.mistral },
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
...{ xAI: defaultSettingsOfProvider.xAI },
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
...{ vLLM: defaultSettingsOfProvider.vLLM },
...readS.settingsOfProvider,
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
gemini: {
...readS.settingsOfProvider.gemini,
models: [
...readS.settingsOfProvider.gemini.models,
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
}
const newModelSelectionOfFeature = {
// A HACK BECAUSE WE ADDED FastApply
...{ 'Apply': null },
...readS.modelSelectionOfFeature,
}
readS = {
...readS,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
}
this.state = _updatedValidatedState(readS)
resolver()
this._onDidChangeState.fire('all')
this._onDidChangeState.fire()
})
}
private async _readState(): Promise<VoidSettingsState> {
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
@@ -143,7 +237,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
const newSettingsOfProvider = {
const newSettingsOfProvider: SettingsOfProvider = {
...this.state.settingsOfProvider,
[providerName]: {
...this.state.settingsOfProvider[providerName],
@@ -153,38 +247,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newGlobalSettings = this.state.globalSettings
// if changed models or enabled a provider, recompute models list
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
const newState: VoidSettingsState = {
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
_modelOptions: newModelsList,
}
// this must go above this.setanythingelse()
this.state = newState
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
if (modelsListChanged) {
for (const featureName of featureNames) {
const currentSelection = newModelSelectionOfFeature[featureName]
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection))
if (selnIdx === -1) {
if (newModelsList.length !== 0)
this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true })
else
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
}
}
}
this.state = _updatedValidatedState(newState)
await this._storeState()
this._onDidChangeState.fire('settingsOfProvider')
this._onDidChangeState.fire()
}
@@ -198,7 +271,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
this.state = newState
await this._storeState()
this._onDidChangeState.fire(['globalSettings', settingName])
this._onDidChangeState.fire()
}
@@ -218,32 +291,31 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
return
await this._storeState()
this._onDidChangeState.fire('modelSelectionOfFeature')
this._onDidChangeState.fire()
}
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) {
setAutodetectedModels(providerName: ProviderName, autodetectedModelNames: string[], logging: object) {
const { models } = this.state.settingsOfProvider[providerName]
const oldModelNames = models.map(m => m.modelName)
const old_names = models.map(m => m.modelName)
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
const newDefaultModels = modelInfoOfAutodetectedModelNames(autodetectedModelNames, { existingModels: models })
const newModels = [
...newDefaultModels,
...models.filter(m => !m.isDefault), // keep any non-default models
...newDefaultModels, // swap out all the default models for the new default models
...models.filter(m => !m.isDefault), // keep any non-default (custom) models
]
this.setSettingOfProvider(providerName, 'models', newModels)
// if the models changed, log it
const new_names = newModels.map(m => m.modelName)
if (!(old_names.length === new_names.length
&& old_names.every((_, i) => old_names[i] === new_names[i])
)) {
this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging })
if (!(oldModelNames.length === new_names.length
&& oldModelNames.every((_, i) => oldModelNames[i] === new_names[i]))
) {
this._metricsService.capture('Autodetect Models', { providerName, newModels: newModels, ...logging })
}
}
toggleModelHidden(providerName: ProviderName, modelName: string) {
@@ -269,7 +341,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
if (existingIdx !== -1) return // if exists, do nothing
const newModels = [
...models,
{ modelName, isDefault: false, isHidden: false }
{ ...developerInfoOfModelName(modelName), modelName, isDefault: false, isHidden: false }
]
this.setSettingOfProvider(providerName, 'models', newModels)
@@ -0,0 +1,419 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { defaultModelsOfProvider } from '../electron-main/llmMessage/MODELS.js';
import { VoidSettingsState } from './voidSettingsService.js'
type UnionOfKeys<T> = T extends T ? keyof T : never;
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
openAI: {
apiKey: '',
},
deepseek: {
apiKey: '',
},
ollama: {
endpoint: 'http://127.0.0.1:11434',
},
vLLM: {
endpoint: 'http://localhost:8000',
},
openRouter: {
apiKey: '',
},
openAICompatible: {
endpoint: '',
apiKey: '',
},
gemini: {
apiKey: '',
},
groq: {
apiKey: '',
},
mistral: {
apiKey: ''
},
xAI: {
apiKey: ''
},
} as const
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
export const localProviderNames = ['ollama', 'vLLM'] satisfies ProviderName[] // all local names
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
}
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
}
export type VoidModelInfo = { // <-- STATEFUL
modelName: string,
isDefault: boolean, // whether or not it's a default for its provider
isHidden: boolean, // whether or not the user is hiding it (switched off)
isAutodetected?: boolean, // whether the model was autodetected by polling
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
type CommonProviderSettings = {
_didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
}
export type SettingsAtProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
[providerName in ProviderName]: SettingsAtProvider<providerName>
}
export type SettingName = keyof SettingsAtProvider<ProviderName>
type DisplayInfoForProviderName = {
title: string,
desc?: string,
}
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
if (providerName === 'anthropic') {
return {
title: 'Anthropic',
}
}
else if (providerName === 'openAI') {
return {
title: 'OpenAI',
}
}
else if (providerName === 'deepseek') {
return {
title: 'DeepSeek.com API',
}
}
else if (providerName === 'openRouter') {
return {
title: 'OpenRouter',
}
}
else if (providerName === 'ollama') {
return {
title: 'Ollama',
}
}
else if (providerName === 'vLLM') {
return {
title: 'vLLM',
}
}
else if (providerName === 'openAICompatible') {
return {
title: 'OpenAI-Compatible',
}
}
else if (providerName === 'gemini') {
return {
title: 'Gemini API',
}
}
else if (providerName === 'groq') {
return {
title: 'Groq.com API',
}
}
else if (providerName === 'mistral') {
return {
title: 'Mistral API',
}
}
else if (providerName === 'xAI') {
return {
title: 'xAI API',
}
}
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
type DisplayInfo = {
title: string;
placeholder: string;
subTextMd?: string;
isPasswordField?: boolean;
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
if (settingName === 'apiKey') {
return {
title: 'API Key',
// **Please follow this convention**:
// The word "key..." here is a placeholder for the hash. For example, sk-ant-key... means the key will look like sk-ant-abcdefg123...
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
providerName === 'openAI' ? 'sk-proj-key...' :
providerName === 'deepseek' ? 'sk-key...' :
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
providerName === 'gemini' ? 'key...' :
providerName === 'groq' ? 'gsk_key...' :
providerName === 'mistral' ? 'key...' :
providerName === 'openAICompatible' ? 'sk-key...' :
providerName === 'xAI' ? 'xai-key...' :
'',
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
providerName === 'deepseek' ? 'Get your [API Key here](https://platform.deepseek.com/api_keys).' :
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' :
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
providerName === 'openAICompatible' ? undefined :
'',
isPasswordField: true,
}
}
else if (settingName === 'endpoint') {
return {
title: providerName === 'ollama' ? 'Endpoint' :
providerName === 'vLLM' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
'(never)',
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
providerName === 'vLLM' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' :
undefined,
}
}
else if (settingName === '_didFillInProviderSettings') {
return {
title: '(never)',
placeholder: '(never)',
}
}
else if (settingName === 'models') {
return {
title: '(never)',
placeholder: '(never)',
}
}
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
}
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
}
const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidModelInfo[] } => {
return {
models: defaultModelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: false,
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
}))
}
}
// used when waiting and for a type reference
export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: {
...defaultCustomSettings,
...defaultProviderSettings.anthropic,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.anthropic),
_didFillInProviderSettings: undefined,
},
openAI: {
...defaultCustomSettings,
...defaultProviderSettings.openAI,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAI),
_didFillInProviderSettings: undefined,
},
deepseek: {
...defaultCustomSettings,
...defaultProviderSettings.deepseek,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.deepseek),
_didFillInProviderSettings: undefined,
},
gemini: {
...defaultCustomSettings,
...defaultProviderSettings.gemini,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.gemini),
_didFillInProviderSettings: undefined,
},
mistral: {
...defaultCustomSettings,
...defaultProviderSettings.mistral,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral),
_didFillInProviderSettings: undefined,
},
xAI: {
...defaultCustomSettings,
...defaultProviderSettings.xAI,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.xAI),
_didFillInProviderSettings: undefined,
},
groq: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.groq,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.groq),
_didFillInProviderSettings: undefined,
},
openRouter: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openRouter),
_didFillInProviderSettings: undefined,
},
openAICompatible: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAICompatible),
_didFillInProviderSettings: undefined,
},
ollama: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.ollama),
_didFillInProviderSettings: undefined,
},
vLLM: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.vLLM,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
_didFillInProviderSettings: undefined,
},
}
export type ModelSelection = { providerName: ProviderName, modelName: string }
export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => {
return m1.modelName === m2.modelName && m1.providerName === m2.providerName
}
// this is a state
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
export type FeatureName = keyof ModelSelectionOfFeature
export const displayInfoOfFeatureName = (featureName: FeatureName) => {
// editor:
if (featureName === 'Autocomplete')
return 'Autocomplete'
else if (featureName === 'Ctrl+K')
return 'Quick Edit'
// sidebar:
else if (featureName === 'Ctrl+L')
return 'Chat'
else if (featureName === 'Apply')
return 'Apply'
else
throw new Error(`Feature Name ${featureName} not allowed`)
}
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = localProviderNames
export type RefreshableProviderName = typeof refreshableProviderNames[number]
// use this in isFeatuerNameDissbled
export const isProviderNameDisabled = (providerName: ProviderName, settingsState: VoidSettingsState) => {
const settingsAtProvider = settingsState.settingsOfProvider[providerName]
const isAutodetected = (refreshableProviderNames as string[]).includes(providerName)
const isDisabled = settingsAtProvider.models.length === 0
if (isDisabled) {
return isAutodetected ? 'providerNotAutoDetected' : (!settingsAtProvider._didFillInProviderSettings ? 'notFilledIn' : 'addModel')
}
return false
}
export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: VoidSettingsState) => {
// if has a selected provider, check if it's enabled
const selectedProvider = settingsState.modelSelectionOfFeature[featureName]
if (selectedProvider) {
const { providerName } = selectedProvider
return isProviderNameDisabled(providerName, settingsState)
}
// if there are any models they can turn on, tell them that
const canTurnOnAModel = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName].models.filter(m => m.isHidden).length !== 0)
if (canTurnOnAModel) return 'needToEnableModel'
// if there are any providers filled in, then they just need to add a model
const anyFilledIn = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName]._didFillInProviderSettings)
if (anyFilledIn) return 'addModel'
return 'addProvider'
}
export type GlobalSettings = {
autoRefreshModels: boolean;
aiInstructions: string;
}
export const defaultGlobalSettings: GlobalSettings = {
autoRefreshModels: true,
aiInstructions: '',
}
export type GlobalSettingName = keyof GlobalSettings
export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[]
@@ -3,10 +3,10 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
@@ -33,7 +33,6 @@ export class VoidUpdateService implements IVoidUpdateService {
}
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
check: IVoidUpdateService['check'] = async () => {
const res = await this.voidUpdateService.check()
@@ -0,0 +1,776 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import OpenAI, { ClientOptions } from 'openai';
import { Model as OpenAIModel } from 'openai/resources/models.js';
import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js';
import { InternalToolInfo, isAToolName } from '../../common/toolsService.js';
import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { prepareMessages } from './preprocessLLMMessages.js';
import Anthropic from '@anthropic-ai/sdk';
import { Ollama } from 'ollama';
export const defaultModelsOfProvider = {
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
'claude-3-5-sonnet-latest',
'claude-3-5-haiku-latest',
'claude-3-opus-latest',
],
openAI: [ // https://platform.openai.com/docs/models/gp
'o1',
'o1-mini',
'o3-mini',
'gpt-4o',
'gpt-4o-mini',
],
deepseek: [ // https://platform.openai.com/docs/models/gp
'deepseek-chat',
'deepseek-reasoner',
],
ollama: [],
vLLM: [],
openRouter: [],
openAICompatible: [],
gemini: [
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-2.0-flash-exp',
'gemini-2.0-flash-thinking-exp-1219',
'learnlm-1.5-pro-experimental'
],
groq: [ // https://console.groq.com/docs/models
"llama3-70b-8192",
"llama-3.3-70b-versatile",
"llama-3.1-8b-instant",
"gemma2-9b-it",
"mixtral-8x7b-32768"
],
mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
"codestral-latest",
"open-codestral-mamba",
"open-mistral-nemo",
"mistral-large-latest",
"pixtral-large-latest",
"ministral-3b-latest",
"ministral-8b-latest",
"mistral-small-latest",
],
xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1
'grok-3-latest',
'grok-2-latest',
],
} satisfies Record<ProviderName, string[]>
type ModelOptions = {
contextWindow: number;
cost: {
input: number;
output: number;
cache_read?: number;
cache_write?: number;
}
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated';
supportsTools: false | 'anthropic-style' | 'openai-style';
supportsFIM: false | 'TODO_FIM_FORMAT';
supportsReasoning: boolean; // not whether it reasons, but whether it outputs reasoning tokens
manualMatchReasoningTokens?: [string, string]; // reasoning tokens if it's an OSS model
}
type ProviderReasoningOptions = {
// include this in payload to get reasoning
input?: { includeInPayload?: { [key: string]: any }, };
// nameOfFieldInDelta: reasoning output is in response.choices[0].delta[deltaReasoningField]
// needsManualParse: whether we must manually parse out the <think> tags
output?:
| { nameOfFieldInDelta?: string, needsManualParse?: undefined, }
| { nameOfFieldInDelta?: undefined, needsManualParse?: true, };
}
type ProviderSettings = {
providerReasoningOptions?: ProviderReasoningOptions;
modelOptions: { [key: string]: ModelOptions };
modelOptionsFallback: (modelName: string) => ModelOptions; // allowed to throw error if modeName is totally invalid
}
type ModelSettingsOfProvider = {
[providerName in ProviderName]: ProviderSettings
}
const modelNotRecognizedErrorMessage = (modelName: string, providerName: ProviderName) => `Void could not find a model matching ${modelName} for ${displayInfoOfProviderName(providerName).title}.`
// ---------------- OPENAI ----------------
const openAIModelOptions = {
"o1": {
contextWindow: 128_000,
cost: { input: 15.00, cache_read: 7.50, output: 60.00, },
supportsFIM: false,
supportsTools: false,
supportsSystemMessage: 'developer-role',
supportsReasoning: false,
},
"o3-mini": {
contextWindow: 200_000,
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
supportsFIM: false,
supportsTools: false,
supportsSystemMessage: 'developer-role',
supportsReasoning: false,
},
"gpt-4o": {
contextWindow: 128_000,
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role',
supportsReasoning: false,
},
} as const
const openAISettings: ProviderSettings = {
modelOptions: openAIModelOptions,
modelOptionsFallback: (modelName) => {
if (modelName.includes('o1')) return openAIModelOptions['o1']
if (modelName.includes('o3-mini')) return openAIModelOptions['o3-mini']
if (modelName.includes('gpt-4o')) return openAIModelOptions['gpt-4o']
throw new Error(modelNotRecognizedErrorMessage(modelName, 'openAI'))
}
}
// ---------------- ANTHROPIC ----------------
const anthropicModelOptions = {
"claude-3-5-sonnet-20241022": {
contextWindow: 200_000,
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
supportsReasoning: false,
},
"claude-3-5-haiku-20241022": {
contextWindow: 200_000,
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
supportsReasoning: false,
},
"claude-3-opus-20240229": {
contextWindow: 200_000,
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
supportsReasoning: false,
},
"claude-3-sonnet-20240229": {
contextWindow: 200_000, cost: { input: 3.00, output: 15.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
supportsReasoning: false,
}
} as const
const anthropicSettings: ProviderSettings = {
modelOptions: anthropicModelOptions,
modelOptionsFallback: (modelName) => {
throw new Error(modelNotRecognizedErrorMessage(modelName, 'anthropic'))
}
}
// ---------------- XAI ----------------
const XAIModelOptions = {
"grok-2-latest": {
contextWindow: 131_072,
cost: { input: 2.00, output: 10.00 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
supportsReasoning: false,
},
} as const
const XAISettings: ProviderSettings = {
modelOptions: XAIModelOptions,
modelOptionsFallback: (modelName) => {
throw new Error(modelNotRecognizedErrorMessage(modelName, 'xAI'))
}
}
const modelSettingsOfProvider: ModelSettingsOfProvider = {
openAI: openAISettings,
anthropic: anthropicSettings,
xAI: XAISettings,
gemini: {
modelOptions: {
}
},
googleVertex: {
},
microsoftAzure: {
},
openRouter: {
providerReasoningOptions: {
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
input: { includeInPayload: { include_reasoning: true } },
output: { nameOfFieldInDelta: 'reasoning' },
}
},
vLLM: {
providerReasoningOptions: {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
output: { nameOfFieldInDelta: 'reasoning_content' },
}
},
deepseek: {
providerReasoningOptions: {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model
output: { nameOfFieldInDelta: 'reasoning_content' },
},
},
ollama: {
providerReasoningOptions: {
// reasoning: we need to filter out reasoning <think> tags manually
output: { needsManualParse: true },
},
},
openAICompatible: {
},
mistral: {
},
groq: {
},
} as const satisfies ModelSettingsOfProvider
const modelOptionsOfProvider = (providerName: ProviderName, modelName: string) => {
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
if (modelName in modelOptions) return modelOptions[modelName]
return modelOptionsFallback(modelName)
}
type InternalCommonMessageParams = {
aiInstructions: string;
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
providerName: ProviderName;
settingsOfProvider: SettingsOfProvider;
modelName: string;
_setAborter: (aborter: () => void) => void;
}
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; tools?: InternalToolInfo[] }
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
const { name, description, params, required } = toolInfo
return {
type: 'function',
function: {
name: name,
description: description,
parameters: {
type: 'object',
properties: params,
required: required,
}
}
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
}
type ToolCallOfIndex = { [index: string]: { name: string, params: string, id: string } }
const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex) => {
return Object.keys(toolCallOfIndex).map(index => {
const tool = toolCallOfIndex[index]
return isAToolName(tool.name) ? { name: tool.name, id: tool.id, params: tool.params } : null
}).filter(t => !!t)
}
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
const commonPayloadOpts: ClientOptions = {
dangerouslyAllowBrowser: true,
...includeInPayload,
}
if (providerName === 'openAI') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'ollama') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
}
else if (providerName === 'vLLM') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
}
else if (providerName === 'openRouter') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({
baseURL: 'https://openrouter.ai/api/v1',
apiKey: thisConfig.apiKey,
defaultHeaders: {
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
'X-Title': 'Void', // Optional. Shows in rankings on openrouter.ai.
},
...commonPayloadOpts,
})
}
else if (providerName === 'gemini') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'deepseek') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'openAICompatible') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'mistral') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'groq') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'xAI') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else throw new Error(`Void providerName was invalid: ${providerName}.`)
}
const manualParseOnText = (
providerName: ProviderName,
modelName: string,
onText_: OnText
): OnText => {
return onText_
}
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
const {
supportsReasoning: modelSupportsReasoning,
supportsSystemMessage,
supportsTools,
} = modelOptionsOfProvider(providerName, modelName)
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, })
const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined
const includeInPayload = modelSupportsReasoning ? {} : modelSettingsOfProvider[providerName].providerReasoningOptions?.input?.includeInPayload || {}
const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {}
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj }
const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].providerReasoningOptions?.output ?? {}
if (needsManualReasoningParse) onText = manualParseOnText(providerName, modelName, onText)
let fullReasoning = ''
let fullText = ''
const toolCallOfIndex: ToolCallOfIndex = {}
openai.chat.completions
.create(options)
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
// tool call
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
const index = tool.index
if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', params: '', id: '' }
toolCallOfIndex[index].name += tool.function?.name ?? ''
toolCallOfIndex[index].params += tool.function?.arguments ?? '';
toolCallOfIndex[index].id = tool.id ?? ''
}
// message
const newText = chunk.choices[0]?.delta?.content ?? ''
fullText += newText
// reasoning
let newReasoning = ''
if (nameOfReasoningFieldInDelta) {
// @ts-ignore
newReasoning = (chunk.choices[0]?.delta?.[nameOfFieldInDelta] || '') + ''
fullReasoning += newReasoning
}
onText({ newText, fullText, newReasoning, fullReasoning })
}
onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) });
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {
if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }); }
else { onError({ message: error + '', fullError: error }); }
})
}
const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal<OpenAIModel>) => {
const onSuccess = ({ models }: { models: OpenAIModel[] }) => {
onSuccess_({ models })
}
const onError = ({ error }: { error: string }) => {
onError_({ error })
}
try {
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
openai.models.list()
.then(async (response) => {
const models: OpenAIModel[] = []
models.push(...response.data)
while (response.hasNextPage()) {
models.push(...(await response.getNextPage()).data)
}
onSuccess({ models })
})
.catch((error) => {
onError({ error: error + '' })
})
}
catch (error) {
onError({ error: error + '' })
}
}
// ------------ OPENAI ------------
const sendOpenAIChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
// ------------ ANTHROPIC ------------
const toAnthropicTool = (toolInfo: InternalToolInfo) => {
const { name, description, params, required } = toolInfo
return {
name: name,
description: description,
input_schema: {
type: 'object',
properties: params,
required: required,
}
} satisfies Anthropic.Messages.Tool
}
const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[]) => {
return content.map(c => {
if (c.type !== 'tool_use') return null
if (!isAToolName(c.name)) return null
return c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null
}).filter(t => !!t)
}
const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
const {
// supportsReasoning: modelSupportsReasoning,
supportsSystemMessage,
supportsTools,
contextWindow,
} = modelOptionsOfProvider(providerName, modelName)
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, })
const thisConfig = settingsOfProvider.anthropic
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
const tools = ((tools_?.length ?? 0) !== 0) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined
const stream = anthropic.messages.stream({
system: separateSystemMessageStr,
messages: messages,
model: modelName,
max_tokens: contextWindow,
tools: tools,
tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time
})
// when receive text
stream.on('text', (newText, fullText) => {
onText({ newText, fullText, newReasoning: '', fullReasoning: '' })
})
// when we get the final message on this stream (or when error/fail)
stream.on('finalMessage', (response) => {
const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n')
const toolCalls = toolCallsFromAnthropicContent(response.content)
onFinalMessage({ fullText: content, toolCalls })
})
// on error
stream.on('error', (error) => {
if (error instanceof Anthropic.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }) }
else { onError({ message: error + '', fullError: error }) }
})
_setAborter(() => stream.controller.abort())
}
// // in future, can do tool_use streaming in anthropic, but it's pretty fast even without streaming...
// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
// stream.on('streamEvent', e => {
// if (e.type === 'content_block_start') {
// if (e.content_block.type !== 'tool_use') return
// const index = e.index
// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
// toolCallOfIndex[index].name += e.content_block.name ?? ''
// toolCallOfIndex[index].args += e.content_block.input ?? ''
// }
// else if (e.type === 'content_block_delta') {
// if (e.delta.type !== 'input_json_delta') return
// toolCallOfIndex[e.index].args += e.delta.partial_json
// }
// })
// ------------ XAI ------------
const sendXAIChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
// ------------ GEMINI ------------
const sendGeminiAPIChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
// ------------ OLLAMA ------------
const newOllamaSDK = ({ endpoint }: { endpoint: string }) => {
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
const ollama = new Ollama({ host: endpoint })
return ollama
}
const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal<OllamaModelResponse>) => {
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
onSuccess_({ models })
}
const onError = ({ error }: { error: string }) => {
onError_({ error })
}
try {
const thisConfig = settingsOfProvider.ollama
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
ollama.list()
.then((response) => {
const { models } = response
onSuccess({ models })
})
.catch((error) => {
onError({ error: error + '' })
})
}
catch (error) {
onError({ error: error + '' })
}
}
const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => {
const thisConfig = settingsOfProvider.ollama
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
let fullText = ''
ollama.generate({
model: modelName,
prompt: messages.prefix,
suffix: messages.suffix,
options: {
stop: messages.stopTokens,
num_predict: 300, // max tokens
// repeat_penalty: 1,
},
raw: true,
stream: true, // stream is not necessary but lets us expose the
})
.then(async stream => {
_setAborter(() => stream.abort())
for await (const chunk of stream) {
const newText = chunk.response
fullText += newText
}
onFinalMessage({ fullText })
})
// when error/fail
.catch((error) => {
onError({ message: error + '', fullError: error })
})
}
// ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat!
const sendOllamaChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
// ------------ OPENAI-COMPATIBLE ------------
// TODO!!! FIM
// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration
const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
// ------------ OPENROUTER ------------
const sendOpenRouterChat = (params: SendChatParams_Internal) => {
_sendOpenAICompatibleChat(params)
}
// ------------ VLLM ------------
const vLLMList = async (params: ListParams_Internal<OpenAIModel>) => {
return _openaiCompatibleList(params)
}
const sendVLLMFIM = (params: SendFIMParams_Internal) => {
// TODO!!!
}
// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration
const sendVLLMChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
// ------------ DEEPSEEK API ------------
const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
// ------------ MISTRAL ------------
const sendMistralAPIChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
// ------------ GROQ ------------
const sendGroqAPIChat = (params: SendChatParams_Internal) => {
return _sendOpenAICompatibleChat(params)
}
/*
FIM:
qwen2.5-coder https://ollama.com/library/qwen2.5-coder/blobs/e94a8ecb9327
<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|>
codestral https://ollama.com/library/codestral/blobs/51707752a87c
[SUFFIX]{{ .Suffix }}[PREFIX] {{ .Prompt }}
deepseek-coder-v2 https://ollama.com/library/deepseek-coder-v2/blobs/22091531faf0
<fim▁begin>{{ .Prompt }}<fim▁hole>{{ .Suffix }}<fim▁end>
starcoder2 https://ollama.com/library/starcoder2/blobs/3b190e68fefe
<file_sep>
<fim_prefix>
{{ .Prompt }}<fim_suffix>{{ .Suffix }}<fim_middle>
<|end_of_text|>
codegemma https://ollama.com/library/codegemma:2b/blobs/48d9a8140749
<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|>
*/
type CallFnOfProvider = {
[providerName in ProviderName]: {
sendChat: (params: SendChatParams_Internal) => void;
sendFIM: ((params: SendFIMParams_Internal) => void) | null;
list: ((params: ListParams_Internal<any>) => void) | null;
}
}
export const sendLLMMessageToProviderImplementation = {
openAI: {
sendChat: sendOpenAIChat,
sendFIM: null,
list: null,
},
anthropic: {
sendChat: sendAnthropicChat,
sendFIM: null,
list: null,
},
xAI: {
sendChat: sendXAIChat,
sendFIM: null,
list: null,
},
gemini: {
sendChat: sendGeminiAPIChat,
sendFIM: null,
list: null,
},
ollama: {
sendChat: sendOllamaChat,
sendFIM: sendOllamaFIM,
list: ollamaList,
},
openAICompatible: {
sendChat: sendOpenAICompatibleChat,
sendFIM: null,
list: null,
},
openRouter: {
sendChat: sendOpenRouterChat,
sendFIM: null,
list: null,
},
vLLM: {
sendChat: sendVLLMChat,
sendFIM: sendVLLMFIM,
list: vLLMList,
},
deepseek: {
sendChat: sendDeepSeekAPIChat,
sendFIM: null,
list: null,
},
groq: {
sendChat: sendGroqAPIChat,
sendFIM: null,
list: null,
},
mistral: {
sendChat: sendMistralAPIChat,
sendFIM: null,
list: null,
},
} satisfies CallFnOfProvider
@@ -0,0 +1,324 @@
import { LLMChatMessage } from '../../common/llmMessageTypes.js';
import { deepClone } from '../../../../../base/common/objects.js';
export const parseObject = (args: unknown) => {
if (typeof args === 'object')
return args
if (typeof args === 'string')
try { return JSON.parse(args) }
catch (e) { return { args } }
return {}
}
const prepareMessages_cloneAndTrim = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => {
const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), }))
return { messages }
}
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
const prepareMessages_systemMessage = ({
messages,
aiInstructions,
supportsSystemMessage,
}: {
messages: LLMChatMessage[],
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
})
: { separateSystemMessageStr?: string, messages: any[] } => {
// find system messages and concatenate them
let systemMessageStr = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n') || undefined;
if (aiInstructions)
systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}`
let separateSystemMessageStr: string | undefined = undefined
// remove all system messages
const newMessages: (LLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system')
// if (!supportsTools) {
// if (!systemMessageStr) systemMessageStr = ''
// systemMessageStr += '' // TODO!!! add tool use system message here
// }
if (systemMessageStr) {
// if supports system message
if (supportsSystemMessage) {
if (supportsSystemMessage === 'separated')
separateSystemMessageStr = systemMessageStr
else if (supportsSystemMessage === 'system-role')
newMessages.unshift({ role: 'system', content: systemMessageStr }) // add new first message
else if (supportsSystemMessage === 'developer-role')
newMessages.unshift({ role: 'developer', content: systemMessageStr }) // add new first message
}
// if does not support system message
else {
if (supportsSystemMessage) {
if (newMessages.length === 0)
newMessages.push({ role: 'user', content: systemMessageStr })
// add system mesasges to first message (should be a user message)
else {
const newFirstMessage = {
role: 'user',
content: (''
+ '<SYSTEM_MESSAGE>\n'
+ systemMessageStr
+ '\n'
+ '</SYSTEM_MESSAGE>\n'
+ newMessages[0].content
)
} as const
newMessages.splice(0, 1) // delete first message
newMessages.unshift(newFirstMessage) // add new first message
}
}
}
}
return { messages: newMessages, separateSystemMessageStr }
}
// convert messages as if about to send to openai
/*
reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
openai MESSAGE (role=assistant):
"tool_calls":[{
"type": "function",
"id": "call_12345xyz",
"function": {
"name": "get_weather",
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
}]
openai RESPONSE (role=user):
{ "role": "tool",
"tool_call_id": tool_call.id,
"content": str(result) }
also see
openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
*/
const prepareMessages_tools_openai = ({ messages }: { messages: LLMChatMessage[], }) => {
const newMessages: (
Exclude<LLMChatMessage, { role: 'assistant' | 'tool' }> | {
role: 'assistant',
content: string;
tool_calls?: {
type: 'function';
id: string;
function: {
name: string;
arguments: string;
}
}[]
} | {
role: 'tool',
id: string; // old val
tool_call_id: string; // new val
content: string;
}
)[] = [];
for (let i = 0; i < messages.length; i += 1) {
const currMsg = messages[i]
if (currMsg.role !== 'tool') {
newMessages.push(currMsg)
continue
}
// edit previous assistant message to have called the tool
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
if (prevMsg?.role === 'assistant') {
prevMsg.tool_calls = [{
type: 'function',
id: currMsg.id,
function: {
name: currMsg.name,
arguments: JSON.stringify(currMsg.params)
}
}]
}
// add the tool
newMessages.push({
role: 'tool',
id: currMsg.id,
content: currMsg.content,
tool_call_id: currMsg.id,
})
}
return { messages: newMessages }
}
// convert messages as if about to send to anthropic
/*
https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
anthropic MESSAGE (role=assistant):
"content": [{
"type": "text",
"text": "<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"
}]
*/
const prepareMessages_tools_anthropic = ({ messages }: { messages: LLMChatMessage[], }) => {
const newMessages: (
Exclude<LLMChatMessage, { role: 'assistant' | 'user' }> | {
role: 'assistant',
content: string | ({
type: 'text';
text: string;
} | {
type: 'tool_use';
name: string;
input: Record<string, any>;
id: string;
})[]
} | {
role: 'user',
content: string | ({
type: 'text';
text: string;
} | {
type: 'tool_result';
tool_use_id: string;
content: string;
})[]
}
)[] = messages;
for (let i = 0; i < newMessages.length; i += 1) {
const currMsg = newMessages[i]
if (currMsg.role !== 'tool') continue
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
if (prevMsg?.role === 'assistant') {
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
}
// turn each tool into a user message with tool results at the end
newMessages[i] = {
role: 'user',
content: [
...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const,
...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [],
]
}
}
return { messages: newMessages }
}
const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => {
if (!supportsTools) {
return { messages: messages }
}
else if (supportsTools === 'anthropic-style') {
return prepareMessages_tools_anthropic({ messages })
}
else if (supportsTools === 'openai-style') {
return prepareMessages_tools_openai({ messages })
}
else {
throw 1
}
}
/*
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"
}
}
}
*/
export const prepareMessages = ({
messages,
aiInstructions,
supportsSystemMessage,
supportsTools,
}: {
messages: LLMChatMessage[],
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
supportsTools: false | 'anthropic-style' | 'openai-style',
}) => {
const { messages: messages1 } = prepareMessages_cloneAndTrim({ messages })
const { messages: messages2, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages1, aiInstructions, supportsSystemMessage })
const { messages: messages3 } = prepareMessages_tools({ messages: messages2, supportsTools })
return {
messages: messages3 as any,
separateSystemMessageStr
} as const
}
@@ -0,0 +1,130 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
import { sendLLMMessageToProviderImplementation } from './MODELS.js';
export const sendLLMMessage = ({
messagesType,
aiInstructions,
messages: messages_,
onText: onText_,
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
logging: { loggingName },
settingsOfProvider,
providerName,
modelName,
tools,
}: SendLLMMessageParams,
metricsService: IMetricsService
) => {
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureLLMEvent = (eventId: string, extras?: object) => {
metricsService.capture(eventId, {
providerName,
modelName,
customEndpointURL: settingsOfProvider[providerName]?.endpoint,
numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length,
...messagesType === 'chatMessages' ? {
numMessages: messages_?.length,
messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
origNumMessages: messages_?.length,
origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
} : messagesType === 'FIMMessage' ? {
prefixLength: messages_.prefix.length,
suffixLength: messages_.suffix.length,
} : {},
...extras,
})
}
const submit_time = new Date()
let _fullTextSoFar = ''
let _aborter: (() => void) | null = null
let _setAborter = (fn: () => void) => { _aborter = fn }
let _didAbort = false
const onText: OnText = (params) => {
const { fullText } = params
if (_didAbort) return
onText_(params)
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls }) => {
if (_didAbort) return
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText, toolCalls })
}
const onError: OnError = ({ message: error, fullError }) => {
if (_didAbort) return
console.error('sendLLMMessage onError:', error)
// handle failed to fetch errors, which give 0 information by design
if (error === 'TypeError: fetch failed')
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.`
captureLLMEvent(`${loggingName} - Error`, { error })
onError_({ message: error, fullError })
}
const onAbort = () => {
captureLLMEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
try { _aborter?.() } // aborter sometimes automatically throws an error
catch (e) { }
_didAbort = true
}
abortRef_.current = onAbort
if (messagesType === 'chatMessages')
captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messages_[messages_.length - 1]?.content.length })
else if (messagesType === 'FIMMessage')
captureLLMEvent(`${loggingName} - Sending FIM`, {}) // TODO!!! add more metrics
try {
const implementation = sendLLMMessageToProviderImplementation[providerName]
if (!implementation) {
onError({ message: `Error: Provider "${providerName}" not recognized.`, fullError: null })
return
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools })
return
}
if (messagesType === 'FIMMessage') {
if (sendFIM) {
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions })
return
}
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
return
}
onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null })
}
catch (error) {
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
// ; (_aborter as any)?.()
// _didAbort = true
}
}
@@ -0,0 +1,142 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// registered in app.ts
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/llmMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
import { sendLLMMessageToProviderImplementation } from './llmMessage/MODELS.js';
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
export class LLMMessageChannel implements IServerChannel {
// sendLLMMessage
private readonly llmMessageEmitters = {
onText: new Emitter<EventLLMMessageOnTextParams>(),
onFinalMessage: new Emitter<EventLLMMessageOnFinalMessageParams>(),
onError: new Emitter<EventLLMMessageOnErrorParams>(),
}
// aborters for above
private readonly abortRefOfRequestId: Record<string, AbortRef> = {}
// list
private readonly listEmitters = {
ollama: {
success: new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>(),
error: new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>(),
},
vLLM: {
success: new Emitter<EventModelListOnSuccessParams<VLLMModelResponse>>(),
error: new Emitter<EventModelListOnErrorParams<VLLMModelResponse>>(),
}
} satisfies {
[providerName: string]: {
success: Emitter<EventModelListOnSuccessParams<any>>,
error: Emitter<EventModelListOnErrorParams<any>>,
}
}
// stupidly, channels can't take in @IService
constructor(
private readonly metricsService: IMetricsService,
) { }
// browser uses this to listen for changes
listen(_: unknown, event: string): Event<any> {
// text
if (event === 'onText_sendLLMMessage') return this.llmMessageEmitters.onText.event;
else if (event === 'onFinalMessage_sendLLMMessage') return this.llmMessageEmitters.onFinalMessage.event;
else if (event === 'onError_sendLLMMessage') return this.llmMessageEmitters.onError.event;
// list
else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event;
else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event;
else if (event === 'onSuccess_list_vLLM') return this.listEmitters.vLLM.success.event;
else if (event === 'onError_list_vLLM') return this.listEmitters.vLLM.error.event;
else throw new Error(`Event not found: ${event}`);
}
// browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages)
async call(_: unknown, command: string, params: any): Promise<any> {
try {
if (command === 'sendLLMMessage') {
this._callSendLLMMessage(params)
}
else if (command === 'abort') {
this._callAbort(params)
}
else if (command === 'ollamaList') {
this._callOllamaList(params)
}
else if (command === 'vLLMList') {
this._callVLLMList(params)
}
else {
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
}
}
catch (e) {
console.log('llmMessageChannel: Call Error:', e)
}
}
// the only place sendLLMMessage is actually called
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
const { requestId } = params;
if (!(requestId in this.abortRefOfRequestId))
this.abortRefOfRequestId[requestId] = { current: null }
const mainThreadParams: SendLLMMessageParams = {
...params,
onText: (p) => { this.llmMessageEmitters.onText.fire({ requestId, ...p }); },
onFinalMessage: (p) => { this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p }); },
onError: (p) => { console.log('sendLLM: firing err'); this.llmMessageEmitters.onError.fire({ requestId, ...p }); },
abortRef: this.abortRefOfRequestId[requestId],
}
sendLLMMessage(mainThreadParams, this.metricsService);
}
_callOllamaList = (params: MainModelListParams<OllamaModelResponse>) => {
const { requestId } = params
const emitters = this.listEmitters.ollama
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
...params,
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
}
sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams)
}
_callVLLMList = (params: MainModelListParams<VLLMModelResponse>) => {
const { requestId } = params
const emitters = this.listEmitters.vLLM
const mainThreadParams: ModelListParams<VLLMModelResponse> = {
...params,
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
}
sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams)
}
private _callAbort(params: MainLLMMessageAbortParams) {
const { requestId } = params;
if (!(requestId in this.abortRefOfRequestId)) return
this.abortRefOfRequestId[requestId].current?.()
delete this.abortRefOfRequestId[requestId]
}
}
@@ -0,0 +1,138 @@
/*--------------------------------------------------------------------------------------
* 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 { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { StorageTarget, StorageScope } from '../../../../platform/storage/common/storage.js';
import { IApplicationStorageMainService } from '../../../../platform/storage/electron-main/storageMainService.js';
import { IMetricsService } from '../common/metricsService.js';
import { PostHog } from 'posthog-node'
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
const _getOSInfo = () => {
try {
const { platform, arch } = process // see platform.ts
return { platform, arch }
}
catch (e) {
return { osInfo: { platform: '??', arch: '??' } }
}
}
const osInfo = _getOSInfo()
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
export class MetricsMainService extends Disposable implements IMetricsService {
_serviceBrand: undefined;
private readonly client: PostHog
private _initProperties: object = {}
// helper - looks like this is stored in a .vscdb file in ~/Library/Application Support/Void
private _memoStorage(key: string, target: StorageTarget, setValIfNotExist?: string) {
const currVal = this._appStorage.get(key, StorageScope.APPLICATION)
if (currVal !== undefined) return currVal
const newVal = setValIfNotExist ?? generateUuid()
this._appStorage.store(key, newVal, StorageScope.APPLICATION, target)
return newVal
}
// this is old, eventually we can just delete this since all the keys will have been transferred over
// returns 'NULL' or the old key
private get oldId() {
// check new storage key first
const newKey = 'void.app.oldMachineId'
const newOldId = this._appStorage.get(newKey, StorageScope.APPLICATION)
if (newOldId) return newOldId
// put old key into new key if didn't already
const oldValue = this._appStorage.get('void.machineId', StorageScope.APPLICATION) ?? 'NULL' // the old way of getting the key
this._appStorage.store(newKey, oldValue, StorageScope.APPLICATION, StorageTarget.MACHINE)
return oldValue
// in a few weeks we can replace above with this
// private get oldId() {
// return this._memoStorage('void.app.oldMachineId', StorageTarget.MACHINE, 'NULL')
// }
}
// the main id
private get distinctId() {
const oldId = this.oldId
const setValIfNotExist = oldId === 'NULL' ? undefined : oldId
return this._memoStorage('void.app.machineId', StorageTarget.MACHINE, setValIfNotExist)
}
// just to see if there are ever multiple machineIDs per userID (instead of this, we should just track by the user's email)
private get userId() {
return this._memoStorage('void.app.userMachineId', StorageTarget.USER)
}
constructor(
@IProductService private readonly _productService: IProductService,
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
@IApplicationStorageMainService private readonly _appStorage: IApplicationStorageMainService,
) {
super()
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', {
host: 'https://us.i.posthog.com',
})
this.initialize() // async
}
async initialize() {
// very important to await whenReady!
await this._appStorage.whenReady
const { commit, version, quality } = this._productService
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
// custom properties we identify
this._initProperties = {
commit,
vscodeVersion: version,
os,
quality,
distinctId: this.distinctId,
distinctIdUser: this.userId,
oldId: this.oldId,
isDevMode,
...osInfo,
}
const identifyMessage = {
distinctId: this.distinctId,
properties: this._initProperties,
}
this.client.identify(identifyMessage)
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
}
capture: IMetricsService['capture'] = (event, params) => {
const capture = { distinctId: this.distinctId, event, properties: params } as const
// console.log('full capture:', capture)
this.client.capture(capture)
}
async getDebuggingProperties() {
return this._initProperties
}
}
@@ -3,10 +3,9 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../base/common/lifecycle.js';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
import { IProductService } from '../../product/common/productService.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IVoidUpdateService } from '../common/voidUpdateService.js';
@@ -44,45 +44,77 @@ export enum ThemeSettings {
}
export enum ThemeSettingDefaults {
COLOR_THEME_DARK = 'Default Dark Modern',
COLOR_THEME_DARK = 'Default Dark+', // Void changed this. this is the default theme
COLOR_THEME_LIGHT = 'Default Light Modern',
COLOR_THEME_HC_DARK = 'Default High Contrast',
COLOR_THEME_HC_LIGHT = 'Default High Contrast Light',
COLOR_THEME_DARK_OLD = 'Default Dark+',
COLOR_THEME_DARK_OLD = 'Default Dark Modern', // Void changed this
COLOR_THEME_LIGHT_OLD = 'Default Light+',
FILE_ICON_THEME = 'vs-seti',
PRODUCT_ICON_THEME = 'Default',
}
export const COLOR_THEME_DARK_INITIAL_COLORS = {
'activityBar.activeBorder': '#0078d4',
'activityBar.background': '#181818',
'activityBar.border': '#2b2b2b',
'activityBar.foreground': '#d7d7d7',
'activityBar.inactiveForeground': '#868686',
'editorGroup.border': '#ffffff17',
'editorGroupHeader.tabsBackground': '#181818',
'editorGroupHeader.tabsBorder': '#2b2b2b',
'statusBar.background': '#181818',
'statusBar.border': '#2b2b2b',
'statusBar.foreground': '#cccccc',
'statusBar.noFolderBackground': '#1f1f1f',
'tab.activeBackground': '#1f1f1f',
'tab.activeBorder': '#1f1f1f',
'tab.activeBorderTop': '#0078d4',
// export const COLOR_THEME_DARK_INITIAL_COLORS = {
// 'activityBar.activeBorder': '#0078d4',
// 'activityBar.background': '#181818',
// 'activityBar.border': '#2b2b2b',
// 'activityBar.foreground': '#d7d7d7',
// 'activityBar.inactiveForeground': '#868686',
// 'editorGroup.border': '#ffffff17',
// 'editorGroupHeader.tabsBackground': '#181818',
// 'editorGroupHeader.tabsBorder': '#2b2b2b',
// 'statusBar.background': '#181818',
// 'statusBar.border': '#2b2b2b',
// 'statusBar.foreground': '#cccccc',
// 'statusBar.noFolderBackground': '#1f1f1f',
// 'tab.activeBackground': '#1f1f1f',
// 'tab.activeBorder': '#1f1f1f',
// 'tab.activeBorderTop': '#0078d4',
// 'tab.activeForeground': '#ffffff',
// 'tab.border': '#2b2b2b',
// 'textLink.foreground': '#4daafc',
// 'titleBar.activeBackground': '#181818',
// 'titleBar.activeForeground': '#cccccc',
// 'titleBar.border': '#2b2b2b',
// 'titleBar.inactiveBackground': '#1f1f1f',
// 'titleBar.inactiveForeground': '#9d9d9d',
// 'welcomePage.tileBackground': '#2b2b2b'
// };
export const COLOR_THEME_DARK_INITIAL_COLORS = { // Void changed this to match dark+
'activityBar.activeBorder': '#ffffff',
'activityBar.background': '#333333',
'activityBar.border': '#454545',
'activityBar.foreground': '#ffffff',
'activityBar.inactiveForeground': '#ffffff66',
'editorGroup.border': '#444444',
'editorGroupHeader.tabsBackground': '#252526',
'editorGroupHeader.tabsBorder': '#252526',
'statusBar.background': '#007ACC',
'statusBar.border': '#454545',
'statusBar.foreground': '#ffffff',
'statusBar.noFolderBackground': '#68217A',
'tab.activeBackground': '#2D2D2D',
'tab.activeBorder': '#ffffff',
'tab.activeBorderTop': '#007ACC',
'tab.activeForeground': '#ffffff',
'tab.border': '#2b2b2b',
'textLink.foreground': '#4daafc',
'titleBar.activeBackground': '#181818',
'titleBar.activeForeground': '#cccccc',
'titleBar.border': '#2b2b2b',
'titleBar.inactiveBackground': '#1f1f1f',
'titleBar.inactiveForeground': '#9d9d9d',
'welcomePage.tileBackground': '#2b2b2b'
'tab.border': '#252526',
'textLink.foreground': '#3794ff',
'titleBar.activeBackground': '#3C3C3C',
'titleBar.activeForeground': '#CCCCCC',
'titleBar.border': '#454545',
'titleBar.inactiveBackground': '#2C2C2C',
'titleBar.inactiveForeground': '#999999',
'welcomePage.tileBackground': '#252526'
};
export const COLOR_THEME_LIGHT_INITIAL_COLORS = {
'activityBar.activeBorder': '#005FB8',
'activityBar.background': '#f8f8f8',
@@ -17,7 +17,6 @@ import './browser/workbench.contribution.js';
//#region --- Void
// Void added this:
import './contrib/void/browser/void.contribution.js';
import '../platform/void/browser/void.contribution.js';
//#endregion